From 9d9dd8a4e6ff9e789937954f305faad22d33a017 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Mon, 27 May 2024 16:16:17 +0200 Subject: [PATCH 01/28] return arbitrary string in response body 'ast' prop --- packages/bridge/src/errors/middleware.ts | 1 + packages/jsts/src/analysis/analysis.ts | 1 + packages/jsts/src/analysis/analyzer.ts | 2 +- packages/jsts/tests/analysis/analyzer.test.ts | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/bridge/src/errors/middleware.ts b/packages/bridge/src/errors/middleware.ts index 385a1664a35..56c29b02730 100644 --- a/packages/bridge/src/errors/middleware.ts +++ b/packages/bridge/src/errors/middleware.ts @@ -92,4 +92,5 @@ export const EMPTY_JSTS_ANALYSIS_OUTPUT: JsTsAnalysisOutput = { cognitiveComplexity: 0, }, cpdTokens: [], + ast: '', }; diff --git a/packages/jsts/src/analysis/analysis.ts b/packages/jsts/src/analysis/analysis.ts index f6a04702b28..5676e24c21b 100644 --- a/packages/jsts/src/analysis/analysis.ts +++ b/packages/jsts/src/analysis/analysis.ts @@ -63,4 +63,5 @@ export interface JsTsAnalysisOutput extends AnalysisOutput { metrics?: Metrics; cpdTokens?: CpdToken[]; ucfgPaths?: string[]; + ast: string; } diff --git a/packages/jsts/src/analysis/analyzer.ts b/packages/jsts/src/analysis/analyzer.ts index 5de8567c712..264f7e5183f 100644 --- a/packages/jsts/src/analysis/analyzer.ts +++ b/packages/jsts/src/analysis/analyzer.ts @@ -84,7 +84,7 @@ function analyzeFile( highlightedSymbols, cognitiveComplexity, ); - return { issues, ucfgPaths, ...extendedMetrics }; + return { issues, ucfgPaths, ...extendedMetrics, ast: 'plop' }; } catch (e) { /** Turns exceptions from TypeScript compiler into "parsing" errors */ if (e.stack.indexOf('typescript.js:') > -1) { diff --git a/packages/jsts/tests/analysis/analyzer.test.ts b/packages/jsts/tests/analysis/analyzer.test.ts index cf9c828cd7f..3cf86cab37e 100644 --- a/packages/jsts/tests/analysis/analyzer.test.ts +++ b/packages/jsts/tests/analysis/analyzer.test.ts @@ -930,4 +930,19 @@ describe('analyzeJSTS', () => { expect(vueIssues).toHaveLength(1); expect(vueIssues[0].message).toEqual('call'); }); + + it('should return the AST along with the issues', async () => { + const rules = [ + { key: 'prefer-default-last', configurations: [], fileTypeTarget: ['MAIN'] }, + ] as RuleConfig[]; + initializeLinter(rules); + initializeLinter([], [], [], 'empty'); + + const filePath = path.join(__dirname, 'fixtures', 'code.js'); + const language = 'js'; + + const { ast } = analyzeJSTS(await jsTsInput({ filePath }), language) as JsTsAnalysisOutput; + + expect(ast).toEqual('plop'); + }); }); From 0feab0d5f331b0e40d023ea8284f810d1adebf33 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Mon, 27 May 2024 19:04:45 +0200 Subject: [PATCH 02/28] reply with form-data server side --- package-lock.json | 66 ++++++++++++++++++++++++++ package.json | 1 + packages/bridge/src/worker.js | 20 +++++++- packages/bridge/tests/router.test.ts | 7 ++- packages/bridge/tests/tools/request.ts | 25 ++++++++-- 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b1077d3176..69e78fd5d7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,7 @@ "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-sonarjs": "^1.0.3", "express": "4.19.2", + "form-data": "4.0.0", "functional-red-black-tree": "1.0.1", "htmlparser2": "9.1.0", "jsx-ast-utils": "3.3.5", @@ -4000,6 +4001,11 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4613,6 +4619,17 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "inBundle": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5078,6 +5095,14 @@ "node": ">= 0.4" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6395,6 +6420,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -15374,6 +15412,11 @@ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "available-typed-arrays": { "version": "1.0.7", "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -15844,6 +15887,14 @@ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -16170,6 +16221,11 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -17170,6 +17226,16 @@ "is-callable": "^1.1.3" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", diff --git a/package.json b/package.json index 18446366fbc..7c2912ecc65 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-sonarjs": "^1.0.3", "express": "4.19.2", + "form-data": "4.0.0", "functional-red-black-tree": "1.0.1", "htmlparser2": "9.1.0", "jsx-ast-utils": "3.3.5", diff --git a/packages/bridge/src/worker.js b/packages/bridge/src/worker.js index ef755a83ad1..08cb140f348 100644 --- a/packages/bridge/src/worker.js +++ b/packages/bridge/src/worker.js @@ -20,6 +20,7 @@ require('module-alias/register'); +const formData = require('form-data'); const { parentPort, workerData } = require('worker_threads'); const { analyzeJSTS, @@ -47,8 +48,19 @@ exports.delegate = function (worker, type) { worker.once('message', message => { switch (message.type) { case 'success': - response.send(message.result); + if (message.format === 'multipart') { + const fd = new formData(); + fd.append('ast', message.result.ast); + delete message.result.ast; + fd.append('json', JSON.stringify(message.result)); + response.set('Content-Type', fd.getHeaders()['content-type']); + response.set('Content-Length', fd.getLengthSync()); + fd.pipe(response); + } else { + response.send(message.result); + } break; + case 'failure': next(message.error); break; @@ -92,7 +104,11 @@ if (parentPort) { await readFileLazily(data); const output = analyzeJSTS(data, 'js'); - parentThread.postMessage({ type: 'success', result: JSON.stringify(output) }); + parentThread.postMessage({ + type: 'success', + result: output, + format: 'multipart', + }); break; } diff --git a/packages/bridge/tests/router.test.ts b/packages/bridge/tests/router.test.ts index 2d652d764ef..1be6e3dc118 100644 --- a/packages/bridge/tests/router.test.ts +++ b/packages/bridge/tests/router.test.ts @@ -114,10 +114,12 @@ describe('router', () => { const filePath = path.join(fixtures, 'file.js'); const fileType = 'MAIN'; const data = { filePath, fileType, tsConfigs: [] }; - const response = (await request(server, '/analyze-js', 'POST', data)) as string; + const response = (await request(server, '/analyze-js', 'POST', data, 'formdata')) as FormData; + + const ast = response.get('ast'); const { issues: [issue], - } = JSON.parse(response); + } = JSON.parse(response.get('json') as string); expect(issue).toEqual( expect.objectContaining({ ruleId: 'prefer-regex-literals', @@ -128,6 +130,7 @@ describe('router', () => { message: `Use a regular expression literal instead of the 'RegExp' constructor.`, }), ); + expect(ast).toEqual('plop'); }); it('should route /analyze-ts requests', async () => { diff --git a/packages/bridge/tests/tools/request.ts b/packages/bridge/tests/tools/request.ts index ed59d0bfc3d..8819c15c611 100644 --- a/packages/bridge/tests/tools/request.ts +++ b/packages/bridge/tests/tools/request.ts @@ -20,15 +20,34 @@ import { AddressInfo } from 'net'; import http from 'http'; +type ResponseType = 'text' | 'formdata'; + /** * Sends an HTTP request to a server's endpoint running on localhost. */ -export async function request(server: http.Server, path: string, method: string, body: any = {}) { - return await fetch(`http://127.0.0.1:${(server.address() as AddressInfo).port}${path}`, { +export async function request( + server: http.Server, + path: string, + method: string, + body: any = {}, + format: ResponseType = 'text', +) { + const res = await fetch(`http://127.0.0.1:${(server.address() as AddressInfo).port}${path}`, { headers: { 'Content-Type': 'application/json', }, method, body: method !== 'GET' ? JSON.stringify(body) : undefined, - }).then(response => response.text()); + }); + + switch (format) { + case 'text': + return res.text(); + break; + case 'formdata': + return res.formData(); + break; + default: + throw new Error(`Unsupported format: ${format}`); + } } From 26b4bd2b13ead87e0d474405d11301c273164e91 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Tue, 28 May 2024 09:59:51 +0200 Subject: [PATCH 03/28] adapt other tests --- packages/bridge/tests/server.test.ts | 18 +++++++++++------- packages/bridge/tests/tools/request.ts | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/bridge/tests/server.test.ts b/packages/bridge/tests/server.test.ts index 8a56cb6ddbc..e3113dbbaf9 100644 --- a/packages/bridge/tests/server.test.ts +++ b/packages/bridge/tests/server.test.ts @@ -21,7 +21,7 @@ import { start } from '../src/server'; import path from 'path'; import { setContext } from '@sonar/shared'; import { AddressInfo } from 'net'; -import { request } from './tools'; +import { BridgeResponseType, request } from './tools'; import http from 'http'; describe('server', () => { @@ -75,10 +75,10 @@ describe('server', () => { }); expect(await requestInitLinter(server, fileType, ruleId)).toBe('OK!'); - + const response = await requestAnalyzeJs(server, fileType, 'formdata'); const { issues: [issue], - } = JSON.parse(await requestAnalyzeJs(server, fileType)); + } = JSON.parse(response.get('json')); expect(issue).toEqual( expect.objectContaining({ ruleId, @@ -99,11 +99,11 @@ describe('server', () => { const fileType = 'MAIN'; await requestInitLinter(server, fileType, ruleId); - const response = await requestAnalyzeJs(server, fileType); + const response = await requestAnalyzeJs(server, fileType, 'formdata'); const { issues: [issue], - } = JSON.parse(response); + } = JSON.parse(response.get('json')); expect(issue).toEqual( expect.objectContaining({ ruleId, @@ -162,11 +162,15 @@ describe('server', () => { }); }); -async function requestAnalyzeJs(server: http.Server, fileType: string): Promise { +async function requestAnalyzeJs( + server: http.Server, + fileType: string, + format: BridgeResponseType = 'text', +): Promise { const filePath = path.join(__dirname, 'fixtures', 'routing.js'); const analysisInput = { filePath, fileType }; - return await request(server, '/analyze-js', 'POST', analysisInput); + return await request(server, '/analyze-js', 'POST', analysisInput, format); } function requestInitLinter(server: http.Server, fileType: string, ruleId: string) { diff --git a/packages/bridge/tests/tools/request.ts b/packages/bridge/tests/tools/request.ts index 8819c15c611..86067e1922c 100644 --- a/packages/bridge/tests/tools/request.ts +++ b/packages/bridge/tests/tools/request.ts @@ -20,7 +20,7 @@ import { AddressInfo } from 'net'; import http from 'http'; -type ResponseType = 'text' | 'formdata'; +export type BridgeResponseType = 'text' | 'formdata'; /** * Sends an HTTP request to a server's endpoint running on localhost. @@ -30,7 +30,7 @@ export async function request( path: string, method: string, body: any = {}, - format: ResponseType = 'text', + format: BridgeResponseType = 'text', ) { const res = await fetch(`http://127.0.0.1:${(server.address() as AddressInfo).port}${path}`, { headers: { From f8ef04b1c278e9e054a9b70c339d77d7d61465b3 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Tue, 28 May 2024 11:19:42 +0200 Subject: [PATCH 04/28] java-part first-try --- packages/bridge/src/worker.js | 1 + .../javascript/bridge/BridgeServerImpl.java | 32 +++++- .../resources/mock-bridge/package-lock.json | 72 +++++++++++++ .../test/resources/mock-bridge/package.json | 15 +++ .../resources/mock-bridge/startServer-fd.js | 101 ++++++++++++++++++ 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 sonar-plugin/bridge/src/test/resources/mock-bridge/package-lock.json create mode 100644 sonar-plugin/bridge/src/test/resources/mock-bridge/package.json create mode 100644 sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js diff --git a/packages/bridge/src/worker.js b/packages/bridge/src/worker.js index 08cb140f348..ba4ace35c10 100644 --- a/packages/bridge/src/worker.js +++ b/packages/bridge/src/worker.js @@ -53,6 +53,7 @@ exports.delegate = function (worker, type) { fd.append('ast', message.result.ast); delete message.result.ast; fd.append('json', JSON.stringify(message.result)); + // this adds the boundary string that will be used to separate the parts response.set('Content-Type', fd.getHeaders()['content-type']); response.set('Content-Length', fd.getLengthSync()); fd.pipe(response); diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index eb7303b6d76..153b4976a57 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -356,13 +356,13 @@ private void initLinter( @Override public AnalysisResponse analyzeJavaScript(JsAnalysisRequest request) throws IOException { String json = GSON.toJson(request); - return response(request(json, "analyze-js"), request.filePath()); + return response(request(json, "analyze-js", true), request.filePath()); } @Override public AnalysisResponse analyzeTypeScript(JsAnalysisRequest request) throws IOException { String json = GSON.toJson(request); - return response(request(json, "analyze-ts"), request.filePath()); + return response(request(json, "analyze-ts", true), request.filePath()); } @Override @@ -384,6 +384,9 @@ public AnalysisResponse analyzeHtml(JsAnalysisRequest request) throws IOExceptio } private String request(String json, String endpoint) throws IOException { + return request(json, endpoint, false); + } + private String request(String json, String endpoint, boolean isFormData) throws IOException { var request = HttpRequest .newBuilder() .uri(url(endpoint)) @@ -394,6 +397,31 @@ private String request(String json, String endpoint) throws IOException { try { var response = client.send(request, BodyHandlers.ofString()); + + if (isFormData) { + String boundary = "--" + response.headers().firstValue("Content-Type") + .orElseThrow(() -> new IllegalStateException("No Content-Type header")) + .split("boundary=")[1]; + String[] parts = response.body().split(boundary); + // Process each part + for (String part : parts) { + // Split the part into headers and body + String[] splitPart = part.split("\r\n\r\n", 2); + if (splitPart.length < 2) + continue; // Skip if there's no body + + String headers = splitPart[0]; + String partBody = splitPart[1]; + + if (headers.contains("json")) { + return partBody; + } + + // Process the part body... + } + } + + // response. return response.body(); } catch (InterruptedException e) { throw handleInterruptedException(e, "Request " + endpoint + " was interrupted."); diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/package-lock.json b/sonar-plugin/bridge/src/test/resources/mock-bridge/package-lock.json new file mode 100644 index 00000000000..3c6a87af767 --- /dev/null +++ b/sonar-plugin/bridge/src/test/resources/mock-bridge/package-lock.json @@ -0,0 +1,72 @@ +{ + "name": "mock-bridge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mock-bridge", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "form-data": "^4.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + } + } +} diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/package.json b/sonar-plugin/bridge/src/test/resources/mock-bridge/package.json new file mode 100644 index 00000000000..7249626f6ae --- /dev/null +++ b/sonar-plugin/bridge/src/test/resources/mock-bridge/package.json @@ -0,0 +1,15 @@ +{ + "name": "mock-bridge", + "version": "1.0.0", + "description": "", + "main": "badResponse.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "form-data": "^4.0.0" + } +} diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js new file mode 100644 index 00000000000..aa2c2f4d973 --- /dev/null +++ b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js @@ -0,0 +1,101 @@ +#!/usr/bin/env node + +const http = require('http'); +const formData = require('form-data'); +const port = process.argv[2]; +const host = process.argv[3]; + +console.log(`allowTsParserJsFiles: ${process.argv[5]}`); +console.log(`sonarlint: ${process.argv[6]}`); +console.log(`debugMemory: ${process.argv[7]}`); +console.log(`additional rules: [${process.argv[8]}]`); + +const requestHandler = (request, response) => { + let data = ''; + request.on('data', chunk => (data += chunk)); + request.on('end', () => { + console.log(data); + + if (request.url === '/status' || request.url === '/new-tsconfig') { + response.writeHead(200, { 'Content-Type': 'text/plain' }); + response.end('OK!'); + } else if (request.url === '/tsconfig-files') { + response.end("{files: ['abs/path/file1', 'abs/path/file2', 'abs/path/file3']}"); + } else if (request.url === '/init-linter') { + response.end('OK!'); + } else if (request.url === '/load-rule-bundles') { + response.end('OK!'); + } else if (request.url === '/close') { + response.end(); + server.close(); + } else if (request.url === '/create-program' && data.includes('invalid')) { + response.end("{ error: 'failed to create program'}"); + } else if (request.url === '/create-program') { + response.end( + "{programId: '42', projectReferences: [], files: ['abs/path/file1', 'abs/path/file2', 'abs/path/file3']}", + ); + } else if (request.url === '/delete-program') { + response.end('OK!'); + } else if (request.url === '/create-tsconfig-file') { + response.end('{"filename":"/path/to/tsconfig.json"}'); + } else { + // /analyze-with-program + // /analyze-js + // /analyze-ts + // /analyze-css + // objects are created to have test coverage + const res = { + issues: [ + { + line: 0, + column: 0, + endLine: 0, + endColumn: 0, + quickFixes: [ + { + edits: [ + { + loc: {}, + }, + ], + }, + ], + }, + ], + highlights: [{ location: { startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 } }], + metrics: {}, + highlightedSymbols: [{}], + cpdTokens: [{}], + }; + const fd = new formData(); + fd.append('ast', 'plop'); + //delete message.result.ast; + fd.append('json', JSON.stringify(res)); + // this adds the boundary string that will be + response.set('Content-Type', fd.getHeaders()['content-type']); + response.set('Content-Length', fd.getLengthSync()); + fd.pipe(response); + } + }); +}; + +const server = http.createServer(requestHandler); +server.keepAliveTimeout = 100; // this is used so server disconnects faster + +server.listen(port, host, err => { + if (err) { + return console.log('something bad happened', err); + } + + console.log(`server is listening on ${host} ${port}`); +}); + +process.on('exit', () => { + console.log(` +Rule | Time (ms) | Relative +:------------------------------------|----------:|--------: +no-commented-code | 633.226 | 16.8% +arguments-order | 398.175 | 10.6% +deprecation | 335.577 | 8.9% + `); +}); From 5aaa68288158d18e177611b0722fe771e69fccc7 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Tue, 28 May 2024 14:51:04 +0200 Subject: [PATCH 05/28] parse JSON multipart correctly for /analyze-js --- .../javascript/bridge/BridgeServerImpl.java | 12 +++++------- .../javascript/bridge/BridgeServerImplTest.java | 2 +- .../test/resources/mock-bridge/startServer-fd.js | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 153b4976a57..26d5e0cff8c 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -362,7 +362,7 @@ public AnalysisResponse analyzeJavaScript(JsAnalysisRequest request) throws IOEx @Override public AnalysisResponse analyzeTypeScript(JsAnalysisRequest request) throws IOException { String json = GSON.toJson(request); - return response(request(json, "analyze-ts", true), request.filePath()); + return response(request(json, "analyze-ts"), request.filePath()); } @Override @@ -403,7 +403,7 @@ private String request(String json, String endpoint, boolean isFormData) throws .orElseThrow(() -> new IllegalStateException("No Content-Type header")) .split("boundary=")[1]; String[] parts = response.body().split(boundary); - // Process each part + for (String part : parts) { // Split the part into headers and body String[] splitPart = part.split("\r\n\r\n", 2); @@ -416,13 +416,11 @@ private String request(String json, String endpoint, boolean isFormData) throws if (headers.contains("json")) { return partBody; } - - // Process the part body... } + throw new IllegalStateException("Data missing from response"); + } else { + return response.body(); } - - // response. - return response.body(); } catch (InterruptedException e) { throw handleInterruptedException(e, "Request " + endpoint + " was interrupted."); } catch (IOException e) { diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java index 945a8b98561..4aaae9f8b84 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java @@ -175,7 +175,7 @@ void should_forward_process_streams() throws Exception { @Test void should_get_answer_from_server() throws Exception { - bridgeServer = createBridgeServer(START_SERVER_SCRIPT); + bridgeServer = createBridgeServer("startServer-fd.js"); bridgeServer.startServer(context, emptyList()); DefaultInputFile inputFile = TestInputFileBuilder diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js index aa2c2f4d973..9d52d991567 100644 --- a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js +++ b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js @@ -38,11 +38,19 @@ const requestHandler = (request, response) => { response.end('OK!'); } else if (request.url === '/create-tsconfig-file') { response.end('{"filename":"/path/to/tsconfig.json"}'); + } else if (['/analyze-css', '/analyze-yaml', '/analyze-html'].includes(request.url)) { + // objects are created to have test coverage + response.end(`{ issues: [{line:0, column:0, endLine:0, endColumn:0, + quickFixes: [ + { + edits: [{ + loc: {}}]}]}], + highlights: [{location: {startLine: 0, startColumn: 0, endLine: 0, endColumn: 0}}], + metrics: {}, highlightedSymbols: [{}], cpdTokens: [{}] }`); } else { // /analyze-with-program // /analyze-js // /analyze-ts - // /analyze-css // objects are created to have test coverage const res = { issues: [ @@ -72,8 +80,10 @@ const requestHandler = (request, response) => { //delete message.result.ast; fd.append('json', JSON.stringify(res)); // this adds the boundary string that will be - response.set('Content-Type', fd.getHeaders()['content-type']); - response.set('Content-Length', fd.getLengthSync()); + response.writeHead(200, { + 'Content-Type': fd.getHeaders()['content-type'], + 'Content-Length': fd.getLengthSync(), + }); fd.pipe(response); } }); From 863f3b6cb2cb277cb3da44e1016fe3ea758da283 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Tue, 28 May 2024 17:24:11 +0200 Subject: [PATCH 06/28] refactor form data parsing code into its own method --- .../javascript/bridge/BridgeServerImpl.java | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 26d5e0cff8c..bbfd27995df 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -32,6 +32,7 @@ import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.file.Path; import java.time.Duration; @@ -397,27 +398,8 @@ private String request(String json, String endpoint, boolean isFormData) throws try { var response = client.send(request, BodyHandlers.ofString()); - if (isFormData) { - String boundary = "--" + response.headers().firstValue("Content-Type") - .orElseThrow(() -> new IllegalStateException("No Content-Type header")) - .split("boundary=")[1]; - String[] parts = response.body().split(boundary); - - for (String part : parts) { - // Split the part into headers and body - String[] splitPart = part.split("\r\n\r\n", 2); - if (splitPart.length < 2) - continue; // Skip if there's no body - - String headers = splitPart[0]; - String partBody = splitPart[1]; - - if (headers.contains("json")) { - return partBody; - } - } - throw new IllegalStateException("Data missing from response"); + return parseFormData(response); } else { return response.body(); } @@ -428,6 +410,28 @@ private String request(String json, String endpoint, boolean isFormData) throws } } + private static String parseFormData(HttpResponse response) { + String boundary = "--" + response.headers().firstValue("Content-Type") + .orElseThrow(() -> new IllegalStateException("No Content-Type header")) + .split("boundary=")[1]; + String[] parts = response.body().split(boundary); + + for (String part : parts) { + // Split the part into headers and body + String[] splitPart = part.split("\r\n\r\n", 2); + if (splitPart.length < 2) + continue; // Skip if there's no body + + String headers = splitPart[0]; + String partBody = splitPart[1]; + + if (headers.contains("json")) { + return partBody; + } + } + throw new IllegalStateException("Data missing from response"); + } + private static IllegalStateException handleInterruptedException( InterruptedException e, String msg From 0e234ea35250cd6cd72f7d43053d0ea562e93f46 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 12:39:27 +0200 Subject: [PATCH 07/28] adapt other bridge calls --- .../javascript/bridge/BridgeServer.java | 22 +++++++++--- .../javascript/bridge/BridgeServerImpl.java | 36 +++++++++++-------- .../bridge/BridgeServerImplTest.java | 4 ++- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java index b9a94353d4f..011589245ea 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java @@ -29,6 +29,9 @@ import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.scanner.ScannerSide; +import org.sonar.plugins.javascript.bridge.BridgeServer.AnalysisResponse; +import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgram; +import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgramRequest; import org.sonarsource.api.sonarlint.SonarLintSide; @ScannerSide @@ -73,14 +76,24 @@ record JsAnalysisRequest(String filePath, String fileType, String language, @Nul record CssAnalysisRequest(String filePath, @Nullable String fileContent, List rules) { } + record BridgeResponse(String json, @Nullable String ast) { + public BridgeResponse(String json) { + this(json, null); + } + } record AnalysisResponse(@Nullable ParsingError parsingError, List issues, List highlights, - List highlightedSymbols, Metrics metrics, List cpdTokens, List ucfgPaths) { - public AnalysisResponse() { - this(null, List.of(), List.of(), List.of(), new Metrics(), List.of(), List.of()); + List highlightedSymbols, Metrics metrics, List cpdTokens, List ucfgPaths, String ast) { + + public AnalysisResponse(AnalysisResponse response, String ast) { + this(response.parsingError, response.issues, response.highlights, response.highlightedSymbols, response.metrics, response.cpdTokens, response.ucfgPaths, ast); + } + + public AnalysisResponse() { + this(null, List.of(), List.of(), List.of(), new Metrics(), List.of(), List.of(), null); } public AnalysisResponse(@Nullable ParsingError parsingError, @Nullable List issues, @Nullable List highlights, - @Nullable List highlightedSymbols, @Nullable Metrics metrics, @Nullable List cpdTokens, List ucfgPaths) { + @Nullable List highlightedSymbols, @Nullable Metrics metrics, @Nullable List cpdTokens, List ucfgPaths, @Nullable String ast) { this.parsingError = parsingError; this.issues = issues != null ? issues : List.of(); this.highlights = highlights != null ? highlights : List.of(); @@ -88,6 +101,7 @@ public AnalysisResponse(@Nullable ParsingError parsingError, @Nullable List response) { + private static BridgeResponse parseFormData(HttpResponse response) { String boundary = "--" + response.headers().firstValue("Content-Type") .orElseThrow(() -> new IllegalStateException("No Content-Type header")) .split("boundary=")[1]; String[] parts = response.body().split(boundary); - + String json = null; + String ast = null; for (String part : parts) { // Split the part into headers and body String[] splitPart = part.split("\r\n\r\n", 2); @@ -426,10 +427,15 @@ private static String parseFormData(HttpResponse response) { String partBody = splitPart[1]; if (headers.contains("json")) { - return partBody; + json = partBody; + } else if (headers.contains("ast")) { + ast = partBody; } } - throw new IllegalStateException("Data missing from response"); + if (json == null || ast == null) { + throw new IllegalStateException("Data missing from response"); + } + return new BridgeResponse(json, ast); } private static IllegalStateException handleInterruptedException( @@ -441,9 +447,9 @@ private static IllegalStateException handleInterruptedException( return new IllegalStateException(msg, e); } - private static AnalysisResponse response(String result, String filePath) { + private static AnalysisResponse response(BridgeResponse result, String filePath) { try { - return GSON.fromJson(result, AnalysisResponse.class); + return new AnalysisResponse(GSON.fromJson(result.json(), AnalysisResponse.class), result.ast()); } catch (JsonSyntaxException e) { String msg = "Failed to parse response for file " + filePath + ": \n-----\n" + result + "\n-----\n"; @@ -471,7 +477,7 @@ public boolean isAlive() { @Override public boolean newTsConfig() { try { - var response = request("", "new-tsconfig"); + var response = request("", "new-tsconfig").json(); return "OK!".equals(response); } catch (IOException e) { LOG.error("Failed to post new-tsconfig", e); @@ -483,7 +489,7 @@ TsConfigResponse tsConfigFiles(String tsconfigAbsolutePath) { String result = null; try { TsConfigRequest tsConfigRequest = new TsConfigRequest(tsconfigAbsolutePath); - result = request(GSON.toJson(tsConfigRequest), "tsconfig-files"); + result = request(GSON.toJson(tsConfigRequest), "tsconfig-files").json(); return GSON.fromJson(result, TsConfigResponse.class); } catch (IOException e) { LOG.error("Failed to request files for tsconfig: " + tsconfigAbsolutePath, e); @@ -512,20 +518,20 @@ public TsConfigFile loadTsConfig(String filename) { @Override public TsProgram createProgram(TsProgramRequest tsProgramRequest) throws IOException { var response = request(GSON.toJson(tsProgramRequest), "create-program"); - return GSON.fromJson(response, TsProgram.class); + return GSON.fromJson(response.json(), TsProgram.class); } @Override public boolean deleteProgram(TsProgram tsProgram) throws IOException { var programToDelete = new TsProgram(tsProgram.programId(), null, null); - var response = request(GSON.toJson(programToDelete), "delete-program"); + var response = request(GSON.toJson(programToDelete), "delete-program").json(); return "OK!".equals(response); } @Override public TsConfigFile createTsConfigFile(String content) throws IOException { var response = request(content, "create-tsconfig-file"); - return GSON.fromJson(response, TsConfigFile.class); + return GSON.fromJson(response.json(), TsConfigFile.class); } private static List emptyListIfNull(@Nullable List list) { diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java index 4aaae9f8b84..bbe96f1f6b9 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java @@ -183,7 +183,9 @@ void should_get_answer_from_server() throws Exception { .setContents("alert('Fly, you fools!')") .build(); JsAnalysisRequest request = createRequest(inputFile); - assertThat(bridgeServer.analyzeJavaScript(request).issues()).hasSize(1); + var response = bridgeServer.analyzeJavaScript(request); + assertThat(response.issues()).hasSize(1); + assertThat(response.ast()).contains("plop"); } @Test From b47c6afb77cc633715edcc2d8f07f58cc8d24ac9 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 14:44:33 +0200 Subject: [PATCH 08/28] remove separate implementation for tests --- .../javascript/bridge/BridgeServerImpl.java | 7 +- .../bridge/BridgeServerImplTest.java | 2 +- .../resources/mock-bridge/startServer-fd.js | 111 ------------------ .../test/resources/mock-bridge/startServer.js | 45 ++++++- 4 files changed, 46 insertions(+), 119 deletions(-) delete mode 100644 sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 33726b6f8aa..8dddcfdffac 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -363,7 +363,7 @@ public AnalysisResponse analyzeJavaScript(JsAnalysisRequest request) throws IOEx @Override public AnalysisResponse analyzeTypeScript(JsAnalysisRequest request) throws IOException { String json = GSON.toJson(request); - return response(request(json, "analyze-ts"), request.filePath()); + return response(request(json, "analyze-ts", true), request.filePath()); } @Override @@ -449,7 +449,10 @@ private static IllegalStateException handleInterruptedException( private static AnalysisResponse response(BridgeResponse result, String filePath) { try { - return new AnalysisResponse(GSON.fromJson(result.json(), AnalysisResponse.class), result.ast()); + return new AnalysisResponse( + GSON.fromJson(result.json(), AnalysisResponse.class), + result.ast() + ); } catch (JsonSyntaxException e) { String msg = "Failed to parse response for file " + filePath + ": \n-----\n" + result + "\n-----\n"; diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java index bbe96f1f6b9..f62a79cf6ba 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java @@ -175,7 +175,7 @@ void should_forward_process_streams() throws Exception { @Test void should_get_answer_from_server() throws Exception { - bridgeServer = createBridgeServer("startServer-fd.js"); + bridgeServer = createBridgeServer(START_SERVER_SCRIPT); bridgeServer.startServer(context, emptyList()); DefaultInputFile inputFile = TestInputFileBuilder diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js deleted file mode 100644 index 9d52d991567..00000000000 --- a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer-fd.js +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env node - -const http = require('http'); -const formData = require('form-data'); -const port = process.argv[2]; -const host = process.argv[3]; - -console.log(`allowTsParserJsFiles: ${process.argv[5]}`); -console.log(`sonarlint: ${process.argv[6]}`); -console.log(`debugMemory: ${process.argv[7]}`); -console.log(`additional rules: [${process.argv[8]}]`); - -const requestHandler = (request, response) => { - let data = ''; - request.on('data', chunk => (data += chunk)); - request.on('end', () => { - console.log(data); - - if (request.url === '/status' || request.url === '/new-tsconfig') { - response.writeHead(200, { 'Content-Type': 'text/plain' }); - response.end('OK!'); - } else if (request.url === '/tsconfig-files') { - response.end("{files: ['abs/path/file1', 'abs/path/file2', 'abs/path/file3']}"); - } else if (request.url === '/init-linter') { - response.end('OK!'); - } else if (request.url === '/load-rule-bundles') { - response.end('OK!'); - } else if (request.url === '/close') { - response.end(); - server.close(); - } else if (request.url === '/create-program' && data.includes('invalid')) { - response.end("{ error: 'failed to create program'}"); - } else if (request.url === '/create-program') { - response.end( - "{programId: '42', projectReferences: [], files: ['abs/path/file1', 'abs/path/file2', 'abs/path/file3']}", - ); - } else if (request.url === '/delete-program') { - response.end('OK!'); - } else if (request.url === '/create-tsconfig-file') { - response.end('{"filename":"/path/to/tsconfig.json"}'); - } else if (['/analyze-css', '/analyze-yaml', '/analyze-html'].includes(request.url)) { - // objects are created to have test coverage - response.end(`{ issues: [{line:0, column:0, endLine:0, endColumn:0, - quickFixes: [ - { - edits: [{ - loc: {}}]}]}], - highlights: [{location: {startLine: 0, startColumn: 0, endLine: 0, endColumn: 0}}], - metrics: {}, highlightedSymbols: [{}], cpdTokens: [{}] }`); - } else { - // /analyze-with-program - // /analyze-js - // /analyze-ts - // objects are created to have test coverage - const res = { - issues: [ - { - line: 0, - column: 0, - endLine: 0, - endColumn: 0, - quickFixes: [ - { - edits: [ - { - loc: {}, - }, - ], - }, - ], - }, - ], - highlights: [{ location: { startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 } }], - metrics: {}, - highlightedSymbols: [{}], - cpdTokens: [{}], - }; - const fd = new formData(); - fd.append('ast', 'plop'); - //delete message.result.ast; - fd.append('json', JSON.stringify(res)); - // this adds the boundary string that will be - response.writeHead(200, { - 'Content-Type': fd.getHeaders()['content-type'], - 'Content-Length': fd.getLengthSync(), - }); - fd.pipe(response); - } - }); -}; - -const server = http.createServer(requestHandler); -server.keepAliveTimeout = 100; // this is used so server disconnects faster - -server.listen(port, host, err => { - if (err) { - return console.log('something bad happened', err); - } - - console.log(`server is listening on ${host} ${port}`); -}); - -process.on('exit', () => { - console.log(` -Rule | Time (ms) | Relative -:------------------------------------|----------:|--------: -no-commented-code | 633.226 | 16.8% -arguments-order | 398.175 | 10.6% -deprecation | 335.577 | 8.9% - `); -}); diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js index b7d06544488..9d52d991567 100644 --- a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js +++ b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js @@ -1,6 +1,7 @@ #!/usr/bin/env node const http = require('http'); +const formData = require('form-data'); const port = process.argv[2]; const host = process.argv[3]; @@ -37,11 +38,7 @@ const requestHandler = (request, response) => { response.end('OK!'); } else if (request.url === '/create-tsconfig-file') { response.end('{"filename":"/path/to/tsconfig.json"}'); - } else { - // /analyze-with-program - // /analyze-js - // /analyze-ts - // /analyze-css + } else if (['/analyze-css', '/analyze-yaml', '/analyze-html'].includes(request.url)) { // objects are created to have test coverage response.end(`{ issues: [{line:0, column:0, endLine:0, endColumn:0, quickFixes: [ @@ -50,6 +47,44 @@ const requestHandler = (request, response) => { loc: {}}]}]}], highlights: [{location: {startLine: 0, startColumn: 0, endLine: 0, endColumn: 0}}], metrics: {}, highlightedSymbols: [{}], cpdTokens: [{}] }`); + } else { + // /analyze-with-program + // /analyze-js + // /analyze-ts + // objects are created to have test coverage + const res = { + issues: [ + { + line: 0, + column: 0, + endLine: 0, + endColumn: 0, + quickFixes: [ + { + edits: [ + { + loc: {}, + }, + ], + }, + ], + }, + ], + highlights: [{ location: { startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 } }], + metrics: {}, + highlightedSymbols: [{}], + cpdTokens: [{}], + }; + const fd = new formData(); + fd.append('ast', 'plop'); + //delete message.result.ast; + fd.append('json', JSON.stringify(res)); + // this adds the boundary string that will be + response.writeHead(200, { + 'Content-Type': fd.getHeaders()['content-type'], + 'Content-Length': fd.getLengthSync(), + }); + fd.pipe(response); } }); }; From 54422f2a67d48c1fb77a75cdd767d96202192f8f Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 15:15:50 +0200 Subject: [PATCH 09/28] generate `ast` prop for other files as well --- packages/bridge/src/worker.js | 6 +++++- packages/bridge/tests/router.test.ts | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/bridge/src/worker.js b/packages/bridge/src/worker.js index ba4ace35c10..dc1002c7beb 100644 --- a/packages/bridge/src/worker.js +++ b/packages/bridge/src/worker.js @@ -124,7 +124,11 @@ if (parentPort) { await readFileLazily(data); const output = analyzeJSTS(data, 'ts'); - parentThread.postMessage({ type: 'success', result: JSON.stringify(output) }); + parentThread.postMessage({ + type: 'success', + result: output, + format: 'multipart', + }); break; } diff --git a/packages/bridge/tests/router.test.ts b/packages/bridge/tests/router.test.ts index 1be6e3dc118..f40fe45060d 100644 --- a/packages/bridge/tests/router.test.ts +++ b/packages/bridge/tests/router.test.ts @@ -115,8 +115,6 @@ describe('router', () => { const fileType = 'MAIN'; const data = { filePath, fileType, tsConfigs: [] }; const response = (await request(server, '/analyze-js', 'POST', data, 'formdata')) as FormData; - - const ast = response.get('ast'); const { issues: [issue], } = JSON.parse(response.get('json') as string); @@ -130,6 +128,7 @@ describe('router', () => { message: `Use a regular expression literal instead of the 'RegExp' constructor.`, }), ); + const ast = response.get('ast'); expect(ast).toEqual('plop'); }); @@ -141,10 +140,10 @@ describe('router', () => { const fileType = 'MAIN'; const tsConfig = path.join(fixtures, 'tsconfig.json'); const data = { filePath, fileType, tsConfigs: [tsConfig] }; - const response = (await request(server, '/analyze-ts', 'POST', data)) as string; + const response = (await request(server, '/analyze-ts', 'POST', data, 'formdata')) as FormData; const { issues: [issue], - } = JSON.parse(response); + } = JSON.parse(response.get('json') as string); expect(issue).toEqual( expect.objectContaining({ ruleId: 'no-duplicate-in-composite', @@ -155,6 +154,8 @@ describe('router', () => { message: `Remove this duplicated type or replace with another one.`, }), ); + const ast = response.get('ast'); + expect(ast).toEqual('plop'); }); it('should route /analyze-with-program requests', async () => { @@ -168,10 +169,16 @@ describe('router', () => { (await request(server, '/create-program', 'POST', { tsConfig })) as string, ); const data = { filePath, fileType, programId }; - const response = (await request(server, '/analyze-with-program', 'POST', data)) as string; + const response = (await request( + server, + '/analyze-with-program', + 'POST', + data, + 'formdata', + )) as FormData; const { issues: [issue], - } = JSON.parse(response); + } = JSON.parse(response.get('json') as string); expect(issue).toEqual( expect.objectContaining({ ruleId: 'no-duplicate-in-composite', From b01dd6841eeb6759687477df1a31feb01e3bcbb2 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 15:21:55 +0200 Subject: [PATCH 10/28] fix issues --- packages/bridge/tests/tools/request.ts | 2 -- .../sonar/plugins/javascript/bridge/BridgeServerImpl.java | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bridge/tests/tools/request.ts b/packages/bridge/tests/tools/request.ts index 86067e1922c..11b7e63b5ec 100644 --- a/packages/bridge/tests/tools/request.ts +++ b/packages/bridge/tests/tools/request.ts @@ -43,10 +43,8 @@ export async function request( switch (format) { case 'text': return res.text(); - break; case 'formdata': return res.formData(); - break; default: throw new Error(`Unsupported format: ${format}`); } diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 8dddcfdffac..445faf903af 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -420,8 +420,10 @@ private static BridgeResponse parseFormData(HttpResponse response) { for (String part : parts) { // Split the part into headers and body String[] splitPart = part.split("\r\n\r\n", 2); - if (splitPart.length < 2) - continue; // Skip if there's no body + if (splitPart.length < 2) { + // Skip if there's no body + continue; + } String headers = splitPart[0]; String partBody = splitPart[1]; From 1472fee91bd3a814d0484704a8e828c371d50cda Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 15:31:34 +0200 Subject: [PATCH 11/28] fix compilation errors --- .../javascript/analysis/AnalysisProcessorTest.java | 8 ++++---- .../sonar/plugins/javascript/analysis/JsTsSensorTest.java | 2 +- .../plugins/javascript/analysis/QuickFixSupportTest.java | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/AnalysisProcessorTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/AnalysisProcessorTest.java index d3a70e252d6..eaebaea83f5 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/AnalysisProcessorTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/AnalysisProcessorTest.java @@ -42,7 +42,7 @@ void should_not_fail_when_invalid_range() { .build(); var location = new Location(1, 2, 1, 1); // invalid range startCol > endCol var highlight = new Highlight(location, ""); - var response = new AnalysisResponse(null, List.of(), List.of(highlight), List.of(), new Metrics(), List.of(), List.of()); + var response = new AnalysisResponse(null, List.of(), List.of(highlight), List.of(), new Metrics(), List.of(), List.of(), null); processor.processResponse(context, mock(JsTsChecks.class), file, response); assertThat(logTester.logs()) .contains("Failed to save highlight in " + file.uri() + " at 1:2-1:1"); @@ -60,14 +60,14 @@ void should_not_fail_when_invalid_symbol() { .build(); var declaration = new Location(1, 2, 1, 1); // invalid range startCol > endCol var symbol = new HighlightedSymbol(declaration, List.of()); - var response = new AnalysisResponse(null, List.of(), List.of(), List.of(symbol), new Metrics(), List.of(), List.of()); + var response = new AnalysisResponse(null, List.of(), List.of(), List.of(symbol), new Metrics(), List.of(), List.of(), null); processor.processResponse(context, mock(JsTsChecks.class), file, response); assertThat(logTester.logs()) .contains("Failed to create symbol declaration in " + file.uri() + " at 1:2-1:1"); context = SensorContextTester.create(baseDir); symbol = new HighlightedSymbol(new Location(1, 1, 1, 2), List.of(new Location(2, 2, 2, 1))); - response = new AnalysisResponse(null, List.of(), List.of(), List.of(symbol), new Metrics(), List.of(), List.of()); + response = new AnalysisResponse(null, List.of(), List.of(), List.of(symbol), new Metrics(), List.of(), List.of(), null); processor.processResponse(context, mock(JsTsChecks.class), file, response); assertThat(logTester.logs()) .contains("Failed to create symbol reference in " + file.uri() + " at 2:2-2:1"); @@ -85,7 +85,7 @@ void should_not_fail_when_invalid_cpd() { .build(); var location = new Location(1, 2, 1, 1); // invalid range startCol > endCol var cpd = new CpdToken(location, "img"); - var response = new AnalysisResponse(null, List.of(), List.of(), List.of(), new Metrics(), List.of(cpd), List.of()); + var response = new AnalysisResponse(null, List.of(), List.of(), List.of(), new Metrics(), List.of(cpd), List.of(), null); processor.processResponse(context, mock(JsTsChecks.class), file, response); assertThat(context.cpdTokens(file.key())).isNull(); assertThat(logTester.logs()) diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JsTsSensorTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JsTsSensorTest.java index 978855d7437..0b4d5c09549 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JsTsSensorTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JsTsSensorTest.java @@ -387,7 +387,7 @@ void should_send_content_when_not_utf8() throws Exception { @Test void should_log_when_failing_typescript() throws Exception { var err = new ParsingError("Debug Failure. False expression.", null, ParsingErrorCode.FAILING_TYPESCRIPT); - var parseError = new AnalysisResponse(err, null, null, null, null, null, null); + var parseError = new AnalysisResponse(err, null, null, null, null, null, null, null); when(bridgeServerMock.analyzeTypeScript(any())).thenReturn(parseError); var file1 = createInputFile(context, "dir/file1.ts"); var file2 = createInputFile(context, "dir/file2.ts"); diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/QuickFixSupportTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/QuickFixSupportTest.java index ce38914ecd0..8b53396072a 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/QuickFixSupportTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/QuickFixSupportTest.java @@ -103,7 +103,7 @@ DefaultSensorContext createContext(Version version) { void test() { var context = createContext(Version.create(6, 3)); - var response = new AnalysisResponse(null, List.of(issueWithQuickFix()), List.of(), List.of(), new Metrics(), List.of(), List.of()); + var response = new AnalysisResponse(null, List.of(issueWithQuickFix()), List.of(), List.of(), new Metrics(), List.of(), List.of(), null); var issueCaptor = ArgumentCaptor.forClass(DefaultSonarLintIssue.class); doNothing().when(sensorStorage).store(issueCaptor.capture()); @@ -135,7 +135,7 @@ static Issue issueWithQuickFix() { @Test void test_old_version() { var context = createContext(Version.create(6, 2)); - var response = new AnalysisResponse(null, List.of(issueWithQuickFix()), List.of(), List.of(), new Metrics(), List.of(), List.of()); + var response = new AnalysisResponse(null, List.of(issueWithQuickFix()), List.of(), List.of(), new Metrics(), List.of(), List.of(), null); var issueCaptor = ArgumentCaptor.forClass(DefaultSonarLintIssue.class); doNothing().when(sensorStorage).store(issueCaptor.capture()); @@ -148,7 +148,7 @@ void test_old_version() { void test_null() { var context = createContext(Version.create(6, 3)); var issue = new Issue(1, 1, 1, 1,"", "no-extra-semi", List.of(), 1.0, List.of()); - var response = new AnalysisResponse(null, List.of(issue), List.of(), List.of(), new Metrics(), List.of(), List.of()); + var response = new AnalysisResponse(null, List.of(issue), List.of(), List.of(), new Metrics(), List.of(), List.of(), null); var issueCaptor = ArgumentCaptor.forClass(DefaultSonarLintIssue.class); doNothing().when(sensorStorage).store(issueCaptor.capture()); From f35478dbe359507bc1a863639666254c15243a0c Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 15:55:07 +0200 Subject: [PATCH 12/28] fix analysis errors --- .../javascript/bridge/BridgeServer.java | 13 ++++----- .../bridge/BridgeServerImplTest.java | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java index 011589245ea..6e5be40c224 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java @@ -29,9 +29,6 @@ import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.scanner.ScannerSide; -import org.sonar.plugins.javascript.bridge.BridgeServer.AnalysisResponse; -import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgram; -import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgramRequest; import org.sonarsource.api.sonarlint.SonarLintSide; @ScannerSide @@ -84,16 +81,18 @@ public BridgeResponse(String json) { record AnalysisResponse(@Nullable ParsingError parsingError, List issues, List highlights, List highlightedSymbols, Metrics metrics, List cpdTokens, List ucfgPaths, String ast) { - public AnalysisResponse(AnalysisResponse response, String ast) { - this(response.parsingError, response.issues, response.highlights, response.highlightedSymbols, response.metrics, response.cpdTokens, response.ucfgPaths, ast); + public AnalysisResponse(AnalysisResponse response, String ast) { + this(response.parsingError, response.issues, response.highlights, response.highlightedSymbols, + response.metrics, response.cpdTokens, response.ucfgPaths, ast); } - public AnalysisResponse() { + public AnalysisResponse() { this(null, List.of(), List.of(), List.of(), new Metrics(), List.of(), List.of(), null); } public AnalysisResponse(@Nullable ParsingError parsingError, @Nullable List issues, @Nullable List highlights, - @Nullable List highlightedSymbols, @Nullable Metrics metrics, @Nullable List cpdTokens, List ucfgPaths, @Nullable String ast) { + @Nullable List highlightedSymbols, @Nullable Metrics metrics, + @Nullable List cpdTokens, List ucfgPaths, @Nullable String ast) { this.parsingError = parsingError; this.issues = issues != null ? issues : List.of(); this.highlights = highlights != null ? highlights : List.of(); diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java index f62a79cf6ba..9814d42f05e 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java @@ -723,6 +723,19 @@ void test_ucfg_bundle_version() throws Exception { .contains("Security Frontend version is available: [some_bundle_version]"); } + @Test + void should_return_an_ast() throws Exception { + bridgeServer = createBridgeServer(START_SERVER_SCRIPT); + bridgeServer.startServer(context, emptyList()); + + DefaultInputFile inputFile = TestInputFileBuilder + .create("foo", "foo.js") + .setContents("alert('Fly, you fools!')") + .build(); + JsAnalysisRequest request = createRequest(inputFile); + var response = bridgeServer.analyzeJavaScript(request); + assertThat(response.ast()).contains("plop"); + } @Test void should_not_deploy_runtime_if_sonar_nodejs_executable_is_set() { var existingDoesntMatterScript = "logging.js"; @@ -737,6 +750,21 @@ void should_not_deploy_runtime_if_sonar_nodejs_executable_is_set() { ); } + @Test + void should_fail_if_form_data_is_malformed() throws Exception { + bridgeServer = createBridgeServer(START_SERVER_SCRIPT); + bridgeServer.startServer(context, emptyList()); + + DefaultInputFile inputFile = TestInputFileBuilder + .create("foo", "foo.js") + .setContents("alert('Fly, you fools!')") + .build(); + JsAnalysisRequest request = createRequest(inputFile); + var response = bridgeServer.analyzeJavaScript(request); + assertThat(response.issues()).hasSize(1); + assertThat(response.ast()).contains("plop"); + } + private BridgeServerImpl createBridgeServer(String startServerScript) { return new BridgeServerImpl( builder(), From 9ff2f4a03632e16a33066a81ee63cd3cd7e9675c Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 15:58:42 +0200 Subject: [PATCH 13/28] add 'form-data' to bundledDependencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2e00c1efc12..b265cc43d59 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "eslint-plugin-react-hooks", "eslint-plugin-sonarjs", "express", + "form-data", "functional-red-black-tree", "htmlparser2", "jsx-ast-utils", From 0208a9222f64a1561737948c9f4171da06d2a822 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 17:10:31 +0200 Subject: [PATCH 14/28] cleanup and fix IT --- .../it/plugin/SonarJsIntegrationTest.java | 35 ++++++++++++++++--- .../javascript/bridge/BridgeServerImpl.java | 2 +- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java b/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java index 643af0163cc..d88aa31d875 100644 --- a/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java +++ b/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java @@ -97,8 +97,9 @@ private void assertAnalyzeJs(Bridge bridge) throws IOException, InterruptedExcep r.fileContent = "function foo() { \n" + " var a; \n" + " var c; // NOSONAR\n" + " var b = 42; \n" + "} \n"; r.filePath = temp.resolve("file.js").toAbsolutePath().toString(); - String response = bridge.request(gson.toJson(r), "analyze-js"); - JsonObject jsonObject = gson.fromJson(response, JsonObject.class); + HttpResponse response = bridge.request(gson.toJson(r), "analyze-js"); + var parsedResponse = parseFormData(response); + JsonObject jsonObject = gson.fromJson(parsedResponse.json(), JsonObject.class); JsonArray issues = jsonObject.getAsJsonArray("issues"); assertThat(issues).hasSize(3); assertThat(issues) @@ -108,6 +109,29 @@ private void assertAnalyzeJs(Bridge bridge) throws IOException, InterruptedExcep JsonObject metrics = jsonObject.getAsJsonObject("metrics"); assertThat(metrics.entrySet()).hasSize(1); assertThat(metrics.get("nosonarLines").getAsJsonArray()).containsExactly(new JsonPrimitive(3)); + assertThat(parsedResponse.ast()).isEqualTo("plop"); + } + + private static BridgeResponse parseFormData(HttpResponse response) { + String boundary = "--" + response.headers().firstValue("Content-Type") + .orElseThrow(() -> new IllegalStateException("No Content-Type header")) + .split("boundary=")[1]; + String[] parts = response.body().split(boundary); + String json = null; + String ast = null; + for (String part : parts) { + // Split the part into headers and body + String[] splitPart = part.split("\r\n\r\n", 2); + String headers = splitPart[0]; + String partBody = splitPart[1]; + + if (headers.contains("json")) { + json = partBody; + } else if (headers.contains("ast")) { + ast = partBody; + } + } + return new BridgeResponse(json, ast); } private void assertStatus(Bridge bridge) { @@ -186,15 +210,14 @@ void start(Path dest) throws IOException { process = pb.start(); } - String request(String json, String endpoint) throws IOException, InterruptedException { + HttpResponse request(String json, String endpoint) throws IOException, InterruptedException { var request = HttpRequest .newBuilder(url(endpoint)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); - var response = client.send(request, HttpResponse.BodyHandlers.ofString()); - return response.body(); + return client.send(request, HttpResponse.BodyHandlers.ofString()); } String status() throws IOException, InterruptedException { @@ -249,4 +272,6 @@ static class Rule { List configurations = Collections.emptyList(); String fileTypeTarget = "MAIN"; } + + record BridgeResponse(String json, String ast) {} } diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 445faf903af..08f65d08eb3 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -387,7 +387,7 @@ public AnalysisResponse analyzeHtml(JsAnalysisRequest request) throws IOExceptio private BridgeResponse request(String json, String endpoint) throws IOException { return request(json, endpoint, false); } - private BridgeResponse request(String json, String endpoint, boolean isFormData) throws IOException { + private BridgeResponse request(String json, String endpoint, boolean isFormData) { var request = HttpRequest .newBuilder() .uri(url(endpoint)) From 1e8ba592a50a6ea5cf06cf85a89140dd45cd31b4 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 17:31:19 +0200 Subject: [PATCH 15/28] put back check --- .../sonar/javascript/it/plugin/SonarJsIntegrationTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java b/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java index d88aa31d875..eb5da2a2bfb 100644 --- a/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java +++ b/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java @@ -122,6 +122,11 @@ private static BridgeResponse parseFormData(HttpResponse response) { for (String part : parts) { // Split the part into headers and body String[] splitPart = part.split("\r\n\r\n", 2); + if (splitPart.length < 2) { + // Skip if there's no body + continue; + } + String headers = splitPart[0]; String partBody = splitPart[1]; From a39d70da640d61c53eb82939061f292938c36889 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Wed, 29 May 2024 17:35:41 +0200 Subject: [PATCH 16/28] put back throws exception --- .../org/sonar/plugins/javascript/bridge/BridgeServerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 08f65d08eb3..445faf903af 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -387,7 +387,7 @@ public AnalysisResponse analyzeHtml(JsAnalysisRequest request) throws IOExceptio private BridgeResponse request(String json, String endpoint) throws IOException { return request(json, endpoint, false); } - private BridgeResponse request(String json, String endpoint, boolean isFormData) { + private BridgeResponse request(String json, String endpoint, boolean isFormData) throws IOException { var request = HttpRequest .newBuilder() .uri(url(endpoint)) From 985a66d1abdc4fa5c823c6c4f26c082c111c4d4f Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 08:30:14 +0200 Subject: [PATCH 17/28] cleanup tests --- .../javascript/bridge/BridgeServerImplTest.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java index 9814d42f05e..0d452c11a2e 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java @@ -185,7 +185,6 @@ void should_get_answer_from_server() throws Exception { JsAnalysisRequest request = createRequest(inputFile); var response = bridgeServer.analyzeJavaScript(request); assertThat(response.issues()).hasSize(1); - assertThat(response.ast()).contains("plop"); } @Test @@ -749,22 +748,6 @@ void should_not_deploy_runtime_if_sonar_nodejs_executable_is_set() { "'" + NODE_EXECUTABLE_PROPERTY + "' is set. Skipping embedded Node.js runtime deployment." ); } - - @Test - void should_fail_if_form_data_is_malformed() throws Exception { - bridgeServer = createBridgeServer(START_SERVER_SCRIPT); - bridgeServer.startServer(context, emptyList()); - - DefaultInputFile inputFile = TestInputFileBuilder - .create("foo", "foo.js") - .setContents("alert('Fly, you fools!')") - .build(); - JsAnalysisRequest request = createRequest(inputFile); - var response = bridgeServer.analyzeJavaScript(request); - assertThat(response.issues()).hasSize(1); - assertThat(response.ast()).contains("plop"); - } - private BridgeServerImpl createBridgeServer(String startServerScript) { return new BridgeServerImpl( builder(), From 096650430279234341c73809dcaec3d2864665a4 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 09:04:55 +0200 Subject: [PATCH 18/28] fix test --- .../com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java b/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java index eb5da2a2bfb..2276dd6bae2 100644 --- a/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java +++ b/its/plugin/tests/src/test/java/com/sonar/javascript/it/plugin/SonarJsIntegrationTest.java @@ -109,7 +109,7 @@ private void assertAnalyzeJs(Bridge bridge) throws IOException, InterruptedExcep JsonObject metrics = jsonObject.getAsJsonObject("metrics"); assertThat(metrics.entrySet()).hasSize(1); assertThat(metrics.get("nosonarLines").getAsJsonArray()).containsExactly(new JsonPrimitive(3)); - assertThat(parsedResponse.ast()).isEqualTo("plop"); + assertThat(parsedResponse.ast()).contains("plop"); } private static BridgeResponse parseFormData(HttpResponse response) { From fb5476f6282280a2b8173b3d855a5088e0a5bf78 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 11:38:07 +0200 Subject: [PATCH 19/28] improve coverage on parseFormData --- .../javascript/bridge/BridgeServerImpl.java | 32 +-------- .../javascript/bridge/FormDataUtils.java | 35 ++++++++++ .../javascript/bridge/FormDataUtilsTest.java | 68 +++++++++++++++++++ 3 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java create mode 100644 sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 445faf903af..f6563563f5a 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -399,7 +399,7 @@ private BridgeResponse request(String json, String endpoint, boolean isFormData) try { var response = client.send(request, BodyHandlers.ofString()); if (isFormData) { - return parseFormData(response); + return FormDataUtils.parseFormData(response); } else { return new BridgeResponse(response.body()); } @@ -410,36 +410,6 @@ private BridgeResponse request(String json, String endpoint, boolean isFormData) } } - private static BridgeResponse parseFormData(HttpResponse response) { - String boundary = "--" + response.headers().firstValue("Content-Type") - .orElseThrow(() -> new IllegalStateException("No Content-Type header")) - .split("boundary=")[1]; - String[] parts = response.body().split(boundary); - String json = null; - String ast = null; - for (String part : parts) { - // Split the part into headers and body - String[] splitPart = part.split("\r\n\r\n", 2); - if (splitPart.length < 2) { - // Skip if there's no body - continue; - } - - String headers = splitPart[0]; - String partBody = splitPart[1]; - - if (headers.contains("json")) { - json = partBody; - } else if (headers.contains("ast")) { - ast = partBody; - } - } - if (json == null || ast == null) { - throw new IllegalStateException("Data missing from response"); - } - return new BridgeResponse(json, ast); - } - private static IllegalStateException handleInterruptedException( InterruptedException e, String msg diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java new file mode 100644 index 00000000000..c9ff4a68af2 --- /dev/null +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java @@ -0,0 +1,35 @@ +package org.sonar.plugins.javascript.bridge; + +import java.net.http.HttpResponse; + +public class FormDataUtils { + public static BridgeServer.BridgeResponse parseFormData(HttpResponse response) { + String boundary = "--" + response.headers().firstValue("Content-Type") + .orElseThrow(() -> new IllegalStateException("No Content-Type header")) + .split("boundary=")[1]; + String[] parts = response.body().split(boundary); + String json = null; + String ast = null; + for (String part : parts) { + // Split the part into headers and body + String[] splitPart = part.split("\r\n\r\n", 2); + if (splitPart.length < 2) { + // Skip if there's no body + continue; + } + + String headers = splitPart[0]; + String partBody = splitPart[1]; + + if (headers.contains("json")) { + json = partBody; + } else if (headers.contains("ast")) { + ast = partBody; + } + } + if (json == null || ast == null) { + throw new IllegalStateException("Data missing from response"); + } + return new BridgeServer.BridgeResponse(json, ast); + } +} diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java new file mode 100644 index 00000000000..af936365a51 --- /dev/null +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java @@ -0,0 +1,68 @@ +package org.sonar.plugins.javascript.bridge; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.plugins.javascript.bridge.FormDataUtils.parseFormData; + +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class FormDataUtilsTest { + + @Test + void should_parse_form_data_into_bridge_response() { + HttpResponse mockResponse = mock(HttpResponse.class); + var values = new HashMap>(); + values.put("Content-Type", List.of("multipart/form-data; boundary=---------------------------9051914041544843365972754266")); + when(mockResponse.headers()).thenReturn(HttpHeaders.of(values, (_a, _b) -> true)); + when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266\r\n" + + "Content-Disposition: form-data; name=\"json\"\r\n" + + "\r\n" + + "{\"hello\":\"worlds\"}\r\n" + + "-----------------------------9051914041544843365972754266\r\n" + + "Content-Disposition: form-data; name=\"ast\"\r\n" + + "\r\n" + + "plop\r\n" + + "-----------------------------9051914041544843365972754266--\r\n"); + BridgeServer.BridgeResponse response = parseFormData(mockResponse); + assertThat(response.ast()).contains("plop"); + assertThat(response.json()).contains("{\"hello\":\"worlds\"}"); + } + + @Test + void should_throw_an_error_if_json_is_missing() { + HttpResponse mockResponse = mock(HttpResponse.class); + var values = new HashMap>(); + values.put("Content-Type", List.of("multipart/form-data; boundary=---------------------------9051914041544843365972754266")); + when(mockResponse.headers()).thenReturn(HttpHeaders.of(values, (_a, _b) -> true)); + when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266\r\n" + + "Content-Disposition: form-data; name=\"ast\"\r\n" + + "\r\n" + + "plop\r\n" + + "-----------------------------9051914041544843365972754266--\r\n"); + assertThatThrownBy(() -> parseFormData(mockResponse)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Data missing from response"); + } + + @Test + void should_throw_an_error_if_ast_is_missing() { + HttpResponse mockResponse = mock(HttpResponse.class); + var values = new HashMap>(); + values.put("Content-Type", List.of("multipart/form-data; boundary=---------------------------9051914041544843365972754266")); + when(mockResponse.headers()).thenReturn(HttpHeaders.of(values, (_a, _b) -> true)); + when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266\r\n" + + "Content-Disposition: form-data; name=\"json\"\r\n" + + "\r\n" + + "{\"hello\":\"worlds\"}\r\n" + + "-----------------------------9051914041544843365972754266--\r\n"); + assertThatThrownBy(() -> parseFormData(mockResponse)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Data missing from response"); + } +} From aa21386170788b5face01c97a67498273d73323e Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 11:57:57 +0200 Subject: [PATCH 20/28] fix issues --- .../org/sonar/plugins/javascript/bridge/BridgeServerImpl.java | 1 - .../org/sonar/plugins/javascript/bridge/FormDataUtils.java | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index f6563563f5a..cbfda8f70fb 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -32,7 +32,6 @@ import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.file.Path; import java.time.Duration; diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java index c9ff4a68af2..e0914fc407c 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java @@ -32,4 +32,8 @@ public static BridgeServer.BridgeResponse parseFormData(HttpResponse res } return new BridgeServer.BridgeResponse(json, ast); } + + private FormDataUtils() { + throw new IllegalStateException("Utility class"); + } } From 0e91b3c9b0996a409d70b3c7f0333d6b8a93bad9 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 11:58:26 +0200 Subject: [PATCH 21/28] remove package.json and its lockfile --- .../resources/mock-bridge/package-lock.json | 72 ------------------- .../test/resources/mock-bridge/package.json | 15 ---- 2 files changed, 87 deletions(-) delete mode 100644 sonar-plugin/bridge/src/test/resources/mock-bridge/package-lock.json delete mode 100644 sonar-plugin/bridge/src/test/resources/mock-bridge/package.json diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/package-lock.json b/sonar-plugin/bridge/src/test/resources/mock-bridge/package-lock.json deleted file mode 100644 index 3c6a87af767..00000000000 --- a/sonar-plugin/bridge/src/test/resources/mock-bridge/package-lock.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "name": "mock-bridge", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mock-bridge", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "form-data": "^4.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - } - } -} diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/package.json b/sonar-plugin/bridge/src/test/resources/mock-bridge/package.json deleted file mode 100644 index 7249626f6ae..00000000000 --- a/sonar-plugin/bridge/src/test/resources/mock-bridge/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "mock-bridge", - "version": "1.0.0", - "description": "", - "main": "badResponse.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "form-data": "^4.0.0" - } -} From 151e5e9080ab7d220768161192c4953c57a942ad Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 13:38:46 +0200 Subject: [PATCH 22/28] fix issue --- .../org/sonar/plugins/javascript/bridge/FormDataUtils.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java index e0914fc407c..e3b074eda7f 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java @@ -3,6 +3,9 @@ import java.net.http.HttpResponse; public class FormDataUtils { + private FormDataUtils() { + throw new IllegalStateException("Utility class"); + } public static BridgeServer.BridgeResponse parseFormData(HttpResponse response) { String boundary = "--" + response.headers().firstValue("Content-Type") .orElseThrow(() -> new IllegalStateException("No Content-Type header")) @@ -32,8 +35,4 @@ public static BridgeServer.BridgeResponse parseFormData(HttpResponse res } return new BridgeServer.BridgeResponse(json, ast); } - - private FormDataUtils() { - throw new IllegalStateException("Utility class"); - } } From 3da0a20504a50fa373d92631ec782e1001a5f7e8 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 13:40:09 +0200 Subject: [PATCH 23/28] refactor mock-bridge startServer.js script to not use 'form-data' lib --- .../test/resources/mock-bridge/startServer.js | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js index 9d52d991567..f95f2d01ee6 100644 --- a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js +++ b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js @@ -75,16 +75,31 @@ const requestHandler = (request, response) => { highlightedSymbols: [{}], cpdTokens: [{}], }; - const fd = new formData(); - fd.append('ast', 'plop'); - //delete message.result.ast; - fd.append('json', JSON.stringify(res)); + const boundary = '---------9051914041544843365972754266'; + const contentTypeHeader = `multipart/form-data; boundary=${boundary}`; + let body = ''; + body += `--${boundary}`; + body += `\r\n`; + body += `Content-Disposition: form-data; name="json"`; + body += `\r\n`; + body += `\r\n`; + body += `${JSON.stringify(res)}`; + body += `\r\n`; + body += `--${boundary}`; + body += `\r\n`; + body += `Content-Disposition: form-data; name="ast"`; + body += `\r\n`; + body += `\r\n`; + body += `plop`; + body += `\r\n`; + body += `--${boundary}--`; + body += `\r\n`; // this adds the boundary string that will be response.writeHead(200, { - 'Content-Type': fd.getHeaders()['content-type'], - 'Content-Length': fd.getLengthSync(), + 'Content-Type': contentTypeHeader, + 'Content-Length': Buffer.byteLength(body, 'utf-8'), }); - fd.pipe(response); + response.end(body); } }); }; From 012cac27bdc849cc3e0864c092fee9960cec004f Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 13:40:55 +0200 Subject: [PATCH 24/28] refactor tests to display newlines clearer --- .../javascript/bridge/FormDataUtilsTest.java | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java index af936365a51..739a500c27d 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java @@ -20,15 +20,22 @@ void should_parse_form_data_into_bridge_response() { var values = new HashMap>(); values.put("Content-Type", List.of("multipart/form-data; boundary=---------------------------9051914041544843365972754266")); when(mockResponse.headers()).thenReturn(HttpHeaders.of(values, (_a, _b) -> true)); - when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266\r\n" + - "Content-Disposition: form-data; name=\"json\"\r\n" + + when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266" + "\r\n" + - "{\"hello\":\"worlds\"}\r\n" + - "-----------------------------9051914041544843365972754266\r\n" + - "Content-Disposition: form-data; name=\"ast\"\r\n" + + "Content-Disposition: form-data; name=\"json\"" + "\r\n" + - "plop\r\n" + - "-----------------------------9051914041544843365972754266--\r\n"); + "\r\n" + + "{\"hello\":\"worlds\"}" + + "\r\n" + + "-----------------------------9051914041544843365972754266" + + "\r\n" + + "Content-Disposition: form-data; name=\"ast\"" + + "\r\n" + + "\r\n" + + "plop" + + "\r\n" + + "-----------------------------9051914041544843365972754266--" + + "\r\n"); BridgeServer.BridgeResponse response = parseFormData(mockResponse); assertThat(response.ast()).contains("plop"); assertThat(response.json()).contains("{\"hello\":\"worlds\"}"); @@ -40,11 +47,15 @@ void should_throw_an_error_if_json_is_missing() { var values = new HashMap>(); values.put("Content-Type", List.of("multipart/form-data; boundary=---------------------------9051914041544843365972754266")); when(mockResponse.headers()).thenReturn(HttpHeaders.of(values, (_a, _b) -> true)); - when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266\r\n" + - "Content-Disposition: form-data; name=\"ast\"\r\n" + + when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266" + "\r\n" + - "plop\r\n" + - "-----------------------------9051914041544843365972754266--\r\n"); + "Content-Disposition: form-data; name=\"ast\"" + + "\r\n" + + "\r\n" + + "plop" + + "\r\n" + + "-----------------------------9051914041544843365972754266--" + + "\r\n"); assertThatThrownBy(() -> parseFormData(mockResponse)) .isInstanceOf(IllegalStateException.class) .hasMessage("Data missing from response"); @@ -56,11 +67,15 @@ void should_throw_an_error_if_ast_is_missing() { var values = new HashMap>(); values.put("Content-Type", List.of("multipart/form-data; boundary=---------------------------9051914041544843365972754266")); when(mockResponse.headers()).thenReturn(HttpHeaders.of(values, (_a, _b) -> true)); - when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266\r\n" + - "Content-Disposition: form-data; name=\"json\"\r\n" + + when(mockResponse.body()).thenReturn("-----------------------------9051914041544843365972754266" + + "\r\n" + + "Content-Disposition: form-data; name=\"json\"" + + "\r\n" + + "\r\n" + + "{\"hello\":\"worlds\"}" + "\r\n" + - "{\"hello\":\"worlds\"}\r\n" + - "-----------------------------9051914041544843365972754266--\r\n"); + "-----------------------------9051914041544843365972754266--" + + "\r\n"); assertThatThrownBy(() -> parseFormData(mockResponse)) .isInstanceOf(IllegalStateException.class) .hasMessage("Data missing from response"); From d7922c5a7e7494ba42f5d302002d268de10c1da2 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 17:17:44 +0200 Subject: [PATCH 25/28] add missing file header --- .../javascript/bridge/FormDataUtils.java | 19 +++++++++++++++++++ .../javascript/bridge/FormDataUtilsTest.java | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java index e3b074eda7f..22016ee48be 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java @@ -1,3 +1,22 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ package org.sonar.plugins.javascript.bridge; import java.net.http.HttpResponse; diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java index 739a500c27d..af6e1461b39 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/FormDataUtilsTest.java @@ -1,3 +1,22 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ package org.sonar.plugins.javascript.bridge; import static org.assertj.core.api.Assertions.assertThat; From 9644ece678316b95e315d9779ab7f420c2c36378 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 17:18:01 +0200 Subject: [PATCH 26/28] add newline --- .../java/org/sonar/plugins/javascript/bridge/FormDataUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java index 22016ee48be..643261caace 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/FormDataUtils.java @@ -25,6 +25,7 @@ public class FormDataUtils { private FormDataUtils() { throw new IllegalStateException("Utility class"); } + public static BridgeServer.BridgeResponse parseFormData(HttpResponse response) { String boundary = "--" + response.headers().firstValue("Content-Type") .orElseThrow(() -> new IllegalStateException("No Content-Type header")) From 666b9b1b4568373dd3b9e6db7b62a6a1baa99426 Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Thu, 30 May 2024 17:33:33 +0200 Subject: [PATCH 27/28] use content-type header instead of boolean to figure out the response kind --- .../javascript/bridge/BridgeServerImpl.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index cbfda8f70fb..3025b866333 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -32,6 +32,7 @@ import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.file.Path; import java.time.Duration; @@ -356,13 +357,13 @@ private void initLinter( @Override public AnalysisResponse analyzeJavaScript(JsAnalysisRequest request) throws IOException { String json = GSON.toJson(request); - return response(request(json, "analyze-js", true), request.filePath()); + return response(request(json, "analyze-js"), request.filePath()); } @Override public AnalysisResponse analyzeTypeScript(JsAnalysisRequest request) throws IOException { String json = GSON.toJson(request); - return response(request(json, "analyze-ts", true), request.filePath()); + return response(request(json, "analyze-ts"), request.filePath()); } @Override @@ -384,9 +385,6 @@ public AnalysisResponse analyzeHtml(JsAnalysisRequest request) throws IOExceptio } private BridgeResponse request(String json, String endpoint) throws IOException { - return request(json, endpoint, false); - } - private BridgeResponse request(String json, String endpoint, boolean isFormData) throws IOException { var request = HttpRequest .newBuilder() .uri(url(endpoint)) @@ -397,7 +395,7 @@ private BridgeResponse request(String json, String endpoint, boolean isFormData) try { var response = client.send(request, BodyHandlers.ofString()); - if (isFormData) { + if (isFormData(response)) { return FormDataUtils.parseFormData(response); } else { return new BridgeResponse(response.body()); @@ -409,6 +407,11 @@ private BridgeResponse request(String json, String endpoint, boolean isFormData) } } + private boolean isFormData(HttpResponse response) { + var contentTypeHeader = response.headers().firstValue("Content-type").orElse(""); + return contentTypeHeader.contains("multipart/form-data"); + } + private static IllegalStateException handleInterruptedException( InterruptedException e, String msg From ee1d7bdf81b0d2dc3b698f528ccd660df5bc34fd Mon Sep 17 00:00:00 2001 From: Ilia Kebets Date: Fri, 31 May 2024 09:10:33 +0200 Subject: [PATCH 28/28] fix issue --- .../org/sonar/plugins/javascript/bridge/BridgeServerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 3025b866333..ac1d014b680 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -407,7 +407,7 @@ private BridgeResponse request(String json, String endpoint) throws IOException } } - private boolean isFormData(HttpResponse response) { + private static boolean isFormData(HttpResponse response) { var contentTypeHeader = response.headers().firstValue("Content-type").orElse(""); return contentTypeHeader.contains("multipart/form-data"); }