From 0b8ad17989fabacca7a80dcc7dc1319f46d1628e Mon Sep 17 00:00:00 2001 From: Charis Kyriakou Date: Fri, 4 Mar 2022 16:52:38 +0000 Subject: [PATCH] Add support for showing code flows --- extensions/ql-vscode/package-lock.json | 52 ++-- extensions/ql-vscode/package.json | 2 +- .../src/remote-queries/sample-data.ts | 198 +++++++++++++++- .../src/remote-queries/sarif-processing.ts | 222 +++++++++++++----- .../remote-queries/shared/analysis-result.ts | 17 +- .../view/AnalysisAlertResult.tsx | 10 + .../src/remote-queries/view/CodePaths.tsx | 180 ++++++++++++++ 7 files changed, 597 insertions(+), 84 deletions(-) create mode 100644 extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx diff --git a/extensions/ql-vscode/package-lock.json b/extensions/ql-vscode/package-lock.json index 94f03dd325b..681c62f49d7 100644 --- a/extensions/ql-vscode/package-lock.json +++ b/extensions/ql-vscode/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@octokit/rest": "^18.5.6", "@primer/octicons-react": "^16.3.0", - "@primer/react": "^34.3.0", + "@primer/react": "^35.0.0-rc.c106d292", "child-process-promise": "^2.2.1", "classnames": "~2.2.6", "d3": "^6.3.1", @@ -647,9 +647,9 @@ } }, "node_modules/@primer/behaviors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.0.3.tgz", - "integrity": "sha512-zh1FKvAXLjKs0rr9Ik9E5M3Q9/npa9hmpuHKmYZn7u9QnSl+X13jFPme3AmtokOlfduFYeHfQyzSIJEhSEVl3w==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.0.tgz", + "integrity": "sha512-Ej2OUc3ZIFaR7WwIUqESO1DTzmpb7wc8xbTVRT9s52jZQDjN7g5iljoK3ocYZm+BIAcKn3MvcwB42hEk4Ga4xQ==" }, "node_modules/@primer/octicons-react": { "version": "16.3.0", @@ -668,11 +668,11 @@ "integrity": "sha512-+Gwo89YK1OFi6oubTlah/zPxxzMNaMLy+inECAYI646KIFdzzhAsKWb3z5tSOu5Ff7no4isRV64rWfMSKLZclw==" }, "node_modules/@primer/react": { - "version": "34.3.0", - "resolved": "https://registry.npmjs.org/@primer/react/-/react-34.3.0.tgz", - "integrity": "sha512-C0qrULg4pcfcPaHwZVyU86HACPP/xVjhNqQaEgvdPV8tFA86ql7EVMuoA973Ke42rUvrhmeO4GBILwQD+3im5A==", + "version": "35.0.0-rc.c106d292", + "resolved": "https://registry.npmjs.org/@primer/react/-/react-35.0.0-rc.c106d292.tgz", + "integrity": "sha512-4CyB2OvwMOt1ZULbOUD6Nz7LnE433OT/h6hJDld/IE4klGAtF1HxDVUG7PfbGmlpw87I79WGjvH2+qx6EIhtZA==", "dependencies": { - "@primer/behaviors": "1.0.3", + "@primer/behaviors": "1.1.0", "@primer/octicons-react": "16.1.1", "@primer/primitives": "7.1.1", "@radix-ui/react-polymorphic": "0.0.14", @@ -688,8 +688,13 @@ "color2k": "1.2.4", "deepmerge": "4.2.2", "focus-visible": "5.2.0", + "history": "5.0.0", "styled-system": "5.1.5" }, + "engines": { + "node": ">=12", + "npm": ">=7" + }, "peerDependencies": { "react": "^17.0.0", "react-dom": "^17.0.0", @@ -6568,6 +6573,14 @@ "he": "bin/he" } }, + "node_modules/history": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz", + "integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -13761,9 +13774,9 @@ } }, "@primer/behaviors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.0.3.tgz", - "integrity": "sha512-zh1FKvAXLjKs0rr9Ik9E5M3Q9/npa9hmpuHKmYZn7u9QnSl+X13jFPme3AmtokOlfduFYeHfQyzSIJEhSEVl3w==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.0.tgz", + "integrity": "sha512-Ej2OUc3ZIFaR7WwIUqESO1DTzmpb7wc8xbTVRT9s52jZQDjN7g5iljoK3ocYZm+BIAcKn3MvcwB42hEk4Ga4xQ==" }, "@primer/octicons-react": { "version": "16.3.0", @@ -13777,11 +13790,11 @@ "integrity": "sha512-+Gwo89YK1OFi6oubTlah/zPxxzMNaMLy+inECAYI646KIFdzzhAsKWb3z5tSOu5Ff7no4isRV64rWfMSKLZclw==" }, "@primer/react": { - "version": "34.3.0", - "resolved": "https://registry.npmjs.org/@primer/react/-/react-34.3.0.tgz", - "integrity": "sha512-C0qrULg4pcfcPaHwZVyU86HACPP/xVjhNqQaEgvdPV8tFA86ql7EVMuoA973Ke42rUvrhmeO4GBILwQD+3im5A==", + "version": "35.0.0-rc.c106d292", + "resolved": "https://registry.npmjs.org/@primer/react/-/react-35.0.0-rc.c106d292.tgz", + "integrity": "sha512-4CyB2OvwMOt1ZULbOUD6Nz7LnE433OT/h6hJDld/IE4klGAtF1HxDVUG7PfbGmlpw87I79WGjvH2+qx6EIhtZA==", "requires": { - "@primer/behaviors": "1.0.3", + "@primer/behaviors": "1.1.0", "@primer/octicons-react": "16.1.1", "@primer/primitives": "7.1.1", "@radix-ui/react-polymorphic": "0.0.14", @@ -13797,6 +13810,7 @@ "color2k": "1.2.4", "deepmerge": "4.2.2", "focus-visible": "5.2.0", + "history": "5.0.0", "styled-system": "5.1.5" }, "dependencies": { @@ -18799,6 +18813,14 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "history": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz", + "integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 1a06cd6929c..3533c797156 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -1052,7 +1052,7 @@ "dependencies": { "@octokit/rest": "^18.5.6", "@primer/octicons-react": "^16.3.0", - "@primer/react": "^34.3.0", + "@primer/react": "^35.0.0-rc.c106d292", "child-process-promise": "^2.2.1", "classnames": "~2.2.6", "d3": "^6.3.1", diff --git a/extensions/ql-vscode/src/remote-queries/sample-data.ts b/extensions/ql-vscode/src/remote-queries/sample-data.ts index 7ae1093a3eb..21b8c267ff1 100644 --- a/extensions/ql-vscode/src/remote-queries/sample-data.ts +++ b/extensions/ql-vscode/src/remote-queries/sample-data.ts @@ -102,6 +102,7 @@ export const sampleRemoteQueryResult: RemoteQueryResult = { const createAnalysisResults = (n: number) => Array(n).fill( { message: 'This shell command depends on an uncontrolled [absolute path](1).', + shortDescription: 'Shell command built from environment values', severity: 'Error', filePath: 'npm-packages/meteor-installer/config.js', codeSnippet: { @@ -113,7 +114,202 @@ const createAnalysisResults = (n: number) => Array(n).fill( startLine: 255, startColumn: 28, endColumn: 62 - } + }, + codeFlows: [ + { + threadFlows: [ + { + filePath: 'npm-packages/meteor-installer/config.js', + highlightedRegion: { + startLine: 35, + startColumn: 20, + endColumn: 61 + }, + codeSnippet: { + startLine: 33, + endLine: 37, + text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/config.js', + highlightedRegion: { + startLine: 35, + startColumn: 7, + endColumn: 61 + }, + codeSnippet: { + startLine: 33, + endLine: 37, + text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/config.js', + highlightedRegion: { + startLine: 40, + startColumn: 3, + endColumn: 13 + }, + codeSnippet: { + startLine: 38, + endLine: 42, + text: ' METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/install.js', + highlightedRegion: { + startLine: 12, + startColumn: 3, + endColumn: 13 + }, + codeSnippet: { + startLine: 10, + endLine: 14, + text: 'const os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/install.js', + highlightedRegion: { + startLine: 11, + startColumn: 7, + endLine: 22, + endColumn: 27 + }, + codeSnippet: { + startLine: 9, + endLine: 24, + text: 'const tmp = require(\'tmp\');\nconst os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n extractPath,\n isWindows,\n rootPath,\n sudoUser,\n isSudo,\n isMac,\n METEOR_LATEST_VERSION,\n} = require(\'./config.js\');\nconst { uninstall } = require(\'./uninstall\');\nconst {\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/install.js', + highlightedRegion: { + startLine: 255, + startColumn: 42, + endColumn: 52 + }, + codeSnippet: { + startLine: 253, + endLine: 257, + text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/install.js', + highlightedRegion: { + startLine: 255, + startColumn: 28, + endColumn: 62 + }, + codeSnippet: { + startLine: 253, + endLine: 257, + text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n' + } + } + ] + }, + { + threadFlows: [ + { + filePath: 'npm-packages/meteor-installer/config2.js', + highlightedRegion: { + startLine: 35, + startColumn: 20, + endColumn: 61 + }, + codeSnippet: { + startLine: 33, + endLine: 37, + text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/config2.js', + highlightedRegion: { + startLine: 35, + startColumn: 7, + endColumn: 61 + }, + codeSnippet: { + startLine: 33, + endLine: 37, + text: '\nconst meteorLocalFolder = \'.meteor\';\nconst meteorPath = path.resolve(rootPath, meteorLocalFolder);\n\nmodule.exports = {\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/config2.js', + highlightedRegion: { + startLine: 40, + startColumn: 3, + endColumn: 13 + }, + codeSnippet: { + startLine: 38, + endLine: 42, + text: ' METEOR_LATEST_VERSION,\n extractPath: rootPath,\n meteorPath,\n release: process.env.INSTALL_METEOR_VERSION || METEOR_LATEST_VERSION,\n rootPath,\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/install2.js', + highlightedRegion: { + startLine: 12, + startColumn: 3, + endColumn: 13 + }, + codeSnippet: { + startLine: 10, + endLine: 14, + text: 'const os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/install2.js', + highlightedRegion: { + startLine: 11, + startColumn: 7, + endLine: 22, + endColumn: 27 + }, + codeSnippet: { + startLine: 9, + endLine: 24, + text: 'const tmp = require(\'tmp\');\nconst os = require(\'os\');\nconst {\n meteorPath,\n release,\n startedPath,\n extractPath,\n isWindows,\n rootPath,\n sudoUser,\n isSudo,\n isMac,\n METEOR_LATEST_VERSION,\n} = require(\'./config.js\');\nconst { uninstall } = require(\'./uninstall\');\nconst {\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/install2.js', + highlightedRegion: { + startLine: 255, + startColumn: 42, + endColumn: 52 + }, + codeSnippet: { + startLine: 253, + endLine: 257, + text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n' + } + }, + { + filePath: 'npm-packages/meteor-installer/install2.js', + highlightedRegion: { + startLine: 255, + startColumn: 28, + endColumn: 62 + }, + codeSnippet: { + startLine: 253, + endLine: 257, + text: ' if (isWindows()) {\n //set for the current session and beyond\n child_process.execSync(`setx path "${meteorPath}/;%path%`);\n return;\n }\n' + } + } + ] + } + ] + } ); diff --git a/extensions/ql-vscode/src/remote-queries/sarif-processing.ts b/extensions/ql-vscode/src/remote-queries/sarif-processing.ts index ded7b001826..a88d0db7639 100644 --- a/extensions/ql-vscode/src/remote-queries/sarif-processing.ts +++ b/extensions/ql-vscode/src/remote-queries/sarif-processing.ts @@ -1,6 +1,6 @@ import * as sarif from 'sarif'; -import { AnalysisAlert, ResultSeverity } from './shared/analysis-result'; +import { AnalysisAlert, CodeFlow, CodeSnippet, HighlightedRegion, ResultSeverity, ThreadFlow } from './shared/analysis-result'; const defaultSeverity = 'Warning'; @@ -36,77 +36,38 @@ export function extractAnalysisAlerts( const severity = tryGetSeverity(run, result) || defaultSeverity; + const { codeFlows, errors: codeFlowsErrors } = extractCodeFlows(result); + if (codeFlowsErrors.length > 0) { + errors.push(...codeFlowsErrors); + continue; + } + if (!result.locations) { errors.push('No locations found in the SARIF result'); continue; } - for (const location of result.locations) { - const contextRegion = location.physicalLocation?.contextRegion; - if (!contextRegion) { - errors.push('No context region found in the SARIF result location'); - continue; - } - if (contextRegion.startLine === undefined) { - errors.push('No start line set for a result context region'); - continue; - } - if (contextRegion.endLine === undefined) { - errors.push('No end line set for a result context region'); - continue; - } - if (!contextRegion.snippet?.text) { - errors.push('No text set for a result context region'); - continue; - } + const rule = tryGetRule(run, result); + const shortDescription = rule?.shortDescription?.text || message; - const region = location.physicalLocation?.region; - if (!region) { - errors.push('No region found in the SARIF result location'); - continue; - } - if (region.startLine === undefined) { - errors.push('No start line set for a result region'); - continue; - } - if (region.startColumn === undefined) { - errors.push('No start column set for a result region'); - continue; - } - if (region.endColumn === undefined) { - errors.push('No end column set for a result region'); - continue; - } + for (const location of result.locations) { + const { processedLocation, errors: locationErrors } = extractLocation(location); - const filePath = location.physicalLocation?.artifactLocation?.uri; - if (!filePath) { - errors.push('No file path found in the SARIF result location'); + if (locationErrors.length > 0) { + errors.push(...locationErrors); continue; } const analysisAlert = { message, - filePath, + shortDescription: shortDescription, + filePath: processedLocation!.filePath, severity, - codeSnippet: { - startLine: contextRegion.startLine, - endLine: contextRegion.endLine, - text: contextRegion.snippet.text - }, - highlightedRegion: { - startLine: region.startLine, - startColumn: region.startColumn, - endLine: region.endLine, - endColumn: region.endColumn - } + codeSnippet: processedLocation!.codeSnippet, + highlightedRegion: processedLocation!.highlightedRegion, + codeFlows: codeFlows }; - const validationErrors = getAlertValidationErrors(analysisAlert); - if (validationErrors.length > 0) { - errors.push(...validationErrors); - continue; - } - alerts.push(analysisAlert); } } @@ -186,18 +147,149 @@ export function tryGetRule( return undefined; } -function getAlertValidationErrors(alert: AnalysisAlert): string[] { - const errors = []; +interface Location { + message?: string; + filePath: string; + codeSnippet: CodeSnippet, + highlightedRegion: HighlightedRegion +} + +function validateContextRegion(contextRegion: sarif.Region | undefined): string[] { + const errors: string[] = []; + + if (!contextRegion) { + errors.push('No context region found in the SARIF result location'); + return errors; + } + if (contextRegion.startLine === undefined) { + errors.push('No start line set for a result context region'); + } + if (contextRegion.endLine === undefined) { + errors.push('No end line set for a result context region'); + } + if (!contextRegion.snippet?.text) { + errors.push('No text set for a result context region'); + } - if (alert.codeSnippet.startLine > alert.codeSnippet.endLine) { - errors.push('The code snippet start line is greater than the end line'); + if (errors.length > 0) { + return errors; } - const highlightedRegion = alert.highlightedRegion; - if (highlightedRegion.endLine === highlightedRegion.startLine && - highlightedRegion.endColumn < highlightedRegion.startColumn) { - errors.push('The highlighted region end column is greater than the start column'); + if (contextRegion.startLine! > contextRegion.endLine!) { + errors.push('Start line is greater than the end line in result context region'); } return errors; } + +function validateRegion(region: sarif.Region | undefined): string[] { + const errors: string[] = []; + + if (!region) { + errors.push('No region found in the SARIF result location'); + return errors; + } + if (region.startLine === undefined) { + errors.push('No start line set for a result region'); + } + if (region.startColumn === undefined) { + errors.push('No start column set for a result region'); + } + if (region.endColumn === undefined) { + errors.push('No end column set for a result region'); + } + + if (errors.length > 0) { + return errors; + } + + if (region.endLine! === region.startLine! && + region.endColumn! < region.startColumn!) { + errors.push('End column is greater than the start column in a result region'); + } + + return errors; +} + +function extractLocation( + location: sarif.Location +): { + processedLocation: Location | undefined, + errors: string[] +} { + const message = location.message?.text; + + const errors = []; + + const contextRegion = location.physicalLocation?.contextRegion; + const contextRegionErrors = validateContextRegion(contextRegion); + errors.push(...contextRegionErrors); + + const region = location.physicalLocation?.region; + const regionErrors = validateRegion(region); + errors.push(...regionErrors); + + const filePath = location.physicalLocation?.artifactLocation?.uri; + if (!filePath) { + errors.push('No file path found in the SARIF result location'); + } + + if (errors.length > 0) { + return { processedLocation: undefined, errors }; + } + + const processedLocation = { + message, + filePath, + codeSnippet: { + startLine: contextRegion!.startLine, + endLine: contextRegion!.endLine, + text: contextRegion!.snippet!.text + }, + highlightedRegion: { + startLine: region!.startLine, + startColumn: region!.startColumn, + endLine: region!.endLine, + endColumn: region!.endColumn + } + } as Location; + + return { processedLocation, errors: [] }; +} + +function extractCodeFlows( + result: sarif.Result +): { + codeFlows: CodeFlow[], + errors: string[] +} { + const codeFlows = []; + const errors = []; + + if (result.codeFlows) { + for (const codeFlow of result.codeFlows) { + const threadFlows = []; + + for (const threadFlow of codeFlow.threadFlows) { + for (const location of threadFlow.locations) { + const { processedLocation, errors: locationErrors } = extractLocation(location); + if (locationErrors.length > 0) { + errors.push(...locationErrors); + continue; + } + + threadFlows.push({ + filePath: processedLocation!.filePath, + codeSnippet: processedLocation!.codeSnippet, + highlightedRegion: processedLocation!.highlightedRegion, + message: processedLocation!.message + } as ThreadFlow); + } + } + + codeFlows.push({ threadFlows } as CodeFlow); + } + } + + return { errors, codeFlows: [] }; +} diff --git a/extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts b/extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts index 061f3aae3ae..8a81ad3e1f5 100644 --- a/extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts +++ b/extensions/ql-vscode/src/remote-queries/shared/analysis-result.ts @@ -8,10 +8,12 @@ export interface AnalysisResults { export interface AnalysisAlert { message: string; + shortDescription: string; severity: ResultSeverity; filePath: string; - codeSnippet: CodeSnippet - highlightedRegion: HighlightedRegion + codeSnippet: CodeSnippet; + highlightedRegion: HighlightedRegion; + codeFlows: CodeFlow[]; } export interface CodeSnippet { @@ -27,4 +29,15 @@ export interface HighlightedRegion { endColumn: number; } +export interface CodeFlow { + threadFlows: ThreadFlow[]; +} + +export interface ThreadFlow { + filePath: string; + codeSnippet: CodeSnippet; + highlightedRegion: HighlightedRegion; + message?: string; +} + export type ResultSeverity = 'Recommendation' | 'Warning' | 'Error'; diff --git a/extensions/ql-vscode/src/remote-queries/view/AnalysisAlertResult.tsx b/extensions/ql-vscode/src/remote-queries/view/AnalysisAlertResult.tsx index 010a01cff67..408511d1666 100644 --- a/extensions/ql-vscode/src/remote-queries/view/AnalysisAlertResult.tsx +++ b/extensions/ql-vscode/src/remote-queries/view/AnalysisAlertResult.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; import { AnalysisAlert } from '../shared/analysis-result'; +import CodePaths from './CodePaths'; import FileCodeSnippet from './FileCodeSnippet'; const AnalysisAlertResult = ({ alert }: { alert: AnalysisAlert }) => { + const showPathsLink = alert.codeFlows.length > 0; return { highlightedRegion={alert.highlightedRegion} severity={alert.severity} message={alert.message} + messageChildren={ + showPathsLink && + } />; }; diff --git a/extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx b/extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx new file mode 100644 index 00000000000..9c9e307ab96 --- /dev/null +++ b/extensions/ql-vscode/src/remote-queries/view/CodePaths.tsx @@ -0,0 +1,180 @@ +import { TriangleDownIcon, XCircleIcon } from '@primer/octicons-react'; +import { ActionList, ActionMenu, Box, Button, Label, Link, Overlay } from '@primer/react'; +import * as React from 'react'; +import { useRef, useState } from 'react'; +import styled from 'styled-components'; +import { CodeFlow, ResultSeverity } from '../shared/analysis-result'; +import FileCodeSnippet from './FileCodeSnippet'; +import SectionTitle from './SectionTitle'; +import VerticalSpace from './VerticalSpace'; + +const StyledCloseButton = styled.button` + position: absolute; + top: 1em; + right: 4em; + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + border: none; + &:focus-visible { + outline: none + } +`; + +const OverlayContainer = styled.div` + padding: 1em; + height: 100%; + width: 100%; + padding: 2em; + position: fixed; + top: 0; + left: 0; + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + overflow-y: scroll; +`; + +const CloseButton = ({ onClick }: { onClick: () => void }) => ( + + + +); + +const CodePath = ({ + codeFlow, + message, + severity +}: { + codeFlow: CodeFlow; + message: string; + severity: ResultSeverity; +}) => { + return <> + {codeFlow.threadFlows.map((threadFlow, index) => +
+ {index !== 0 && } + + + + Step {index + 1} + + {index === 0 && + + + + } + {index === codeFlow.threadFlows.length - 1 && + + + + } + + + + +
+ )} + ; +}; + +const getCodeFlowName = (codeFlow: CodeFlow) => { + const filePath = codeFlow.threadFlows[codeFlow.threadFlows.length - 1].filePath; + return filePath.substring(filePath.lastIndexOf('/') + 1); +}; + +const Menu = ({ + codeFlows, + setSelectedCodeFlow +}: { + codeFlows: CodeFlow[], + setSelectedCodeFlow: (value: React.SetStateAction) => void +}) => { + return + + + + + + {codeFlows.map((codeFlow, index) => + { setSelectedCodeFlow(codeFlow); }}> + {getCodeFlowName(codeFlow)} + + )} + + + ; +}; + +const CodePaths = ({ + codeFlows, + ruleDescription, + message, + severity +}: { + codeFlows: CodeFlow[], + ruleDescription: string, + message: string, + severity: ResultSeverity +}) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedCodeFlow, setSelectedCodeFlow] = useState(codeFlows[0]); + + const anchorRef = useRef(null); + const linkRef = useRef(null); + + const closeOverlay = () => setIsOpen(false); + + return ( + + setIsOpen(true)} + ref={linkRef} + sx={{ cursor: 'pointer' }}> + Show paths + + {isOpen && ( + + + + + {ruleDescription} + + + + + {codeFlows.length} paths available: {selectedCodeFlow.threadFlows.length} steps in + + + + + + + + + + + + + + )} + + ); +}; + +export default CodePaths;