Skip to content

Commit

Permalink
JS-554 Refactor JS ruling
Browse files Browse the repository at this point in the history
  • Loading branch information
vdiez committed Feb 23, 2025
1 parent 85feb29 commit 2233eec
Show file tree
Hide file tree
Showing 104 changed files with 733 additions and 1,166 deletions.
10 changes: 6 additions & 4 deletions packages/bridge/src/delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,29 @@ import { JsTsAnalysisOutputWithAst } from '../../jsts/src/analysis/analysis.js';
import { handleRequest } from './handle-request.js';
import { AnalysisOutput } from '../../shared/src/types/analysis.js';
import { RequestResult, RequestType } from './request.js';
import { WorkerData } from '../../shared/src/helpers/worker.js';

/**
* Returns a delegate function to handle an HTTP request
*/
export function createDelegator(worker: Worker | undefined) {
export function createDelegator(worker: Worker | undefined, workerData?: WorkerData) {
return function (type: RequestType) {
return worker ? createWorkerHandler(worker, type) : createHandler(type);
return worker ? createWorkerHandler(worker, type) : createHandler(type, workerData);
};
}

/**
* Handler to analyze in the same thread as HTTP server. Used for testing purposes
* @param type
* @param workerData
*/
function createHandler(type: RequestType) {
function createHandler(type: RequestType, workerData?: WorkerData) {
return async (
request: express.Request,
response: express.Response,
next: express.NextFunction,
) => {
handleResult(await handleRequest({ type, data: request.body }), response, next);
handleResult(await handleRequest({ type, data: request.body }, workerData), response, next);
};
}

Expand Down
34 changes: 13 additions & 21 deletions packages/bridge/src/errors/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import express from 'express';
import { ErrorCode } from '../../../shared/src/errors/error.js';
import { error } from '../../../shared/src/helpers/logging.js';
import { JsTsAnalysisOutput } from '../../../jsts/src/analysis/analysis.js';

/**
* Express.js middleware for handling error while serving requests.
Expand Down Expand Up @@ -67,28 +66,21 @@ export function parseParsingError(error: {
code: error.code,
line: error.data?.line,
},
...EMPTY_JSTS_ANALYSIS_OUTPUT,
};
}

/**
* An empty JavaScript / TypeScript analysis output sent back on paring errors.
* Creates a S2260 issue from the parsing error
*/
export const EMPTY_JSTS_ANALYSIS_OUTPUT: JsTsAnalysisOutput = {
issues: [],
highlights: [],
highlightedSymbols: [],
metrics: {
ncloc: [],
commentLines: [],
nosonarLines: [],
executableLines: [],
functions: 0,
statements: 0,
classes: 0,
complexity: 0,
cognitiveComplexity: 0,
},
cpdTokens: [],
ast: new Uint8Array(),
};
export function createParsingIssue({
parsingError: { line, message },
}: {
parsingError: { line?: number; message: string };
}) {
return {
language: 'js',
ruleId: 'S2260',
line,
message,
} as const;
}
27 changes: 13 additions & 14 deletions packages/bridge/src/handle-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,41 +28,40 @@ import {
} from '../../jsts/src/program/program.js';
import { Linter } from '../../jsts/src/linter/linter.js';
import { clearTypeScriptESLintParserCaches } from '../../jsts/src/parsers/eslint.js';
import {
BridgeRequest,
fillLanguage,
readFileLazily,
RequestResult,
serializeError,
} from './request.js';
import { BridgeRequest, RequestResult, serializeError } from './request.js';
import { WorkerData } from '../../shared/src/helpers/worker.js';

export async function handleRequest(request: BridgeRequest): Promise<RequestResult> {
export async function handleRequest(
request: BridgeRequest,
workerData?: WorkerData,
): Promise<RequestResult> {
try {
switch (request.type) {
case 'on-init-linter': {
await Linter.initialize(request.data);
return { type: 'success', result: 'OK!' };
}
case 'on-analyze-jsts': {
const output = analyzeJSTS(fillLanguage(await readFileLazily(request.data)));
const output = await analyzeJSTS(request.data);
return {
type: 'success',
result: output,
};
}
case 'on-create-program': {
logHeapStatistics();
logHeapStatistics(workerData?.debugMemory);
const { programId, files, projectReferences, missingTsConfig } = createAndSaveProgram(
request.data.tsConfig,
);
logHeapStatistics(workerData?.debugMemory);
return {
type: 'success',
result: { programId, files, projectReferences, missingTsConfig },
};
}
case 'on-delete-program': {
deleteProgram(request.data.programId);
logHeapStatistics();
logHeapStatistics(workerData?.debugMemory);
return { type: 'success', result: 'OK!' };
}
case 'on-create-tsconfig-file': {
Expand All @@ -88,16 +87,16 @@ export async function handleRequest(request: BridgeRequest): Promise<RequestResu
};
}
case 'on-analyze-css': {
const output = await analyzeCSS(await readFileLazily(request.data));
const output = await analyzeCSS(request.data);
return { type: 'success', result: output };
}
case 'on-analyze-yaml': {
const output = analyzeYAML(await readFileLazily(request.data));
const output = await analyzeYAML(request.data);
return { type: 'success', result: output };
}

case 'on-analyze-html': {
const output = analyzeHTML(await readFileLazily(request.data));
const output = await analyzeHTML(request.data);
return { type: 'success', result: output };
}
case 'on-analyze-project': {
Expand Down
6 changes: 2 additions & 4 deletions packages/bridge/src/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { constants, NodeGCPerformanceDetail, PerformanceObserver } from 'perf_ho
import { debug, error, info, warn } from '../../shared/src/helpers/logging.js';

const MB = 1024 * 1024;
let debugMemory = false;

export function logMemoryConfiguration() {
const osMem = Math.floor(os.totalmem() / MB);
Expand Down Expand Up @@ -83,7 +82,6 @@ export function logMemoryError(err: any) {
}

export function registerGarbageCollectionObserver() {
debugMemory = true;
const obs = new PerformanceObserver(items => {
items
.getEntries()
Expand All @@ -94,13 +92,13 @@ export function registerGarbageCollectionObserver() {
.forEach(item => {
debug(`Major GC event`);
debug(JSON.stringify(item));
logHeapStatistics();
logHeapStatistics(true);
});
});
obs.observe({ entryTypes: ['gc'] });
}

export function logHeapStatistics() {
export function logHeapStatistics(debugMemory = false) {
if (debugMemory) {
debug(JSON.stringify(v8.getHeapStatistics()));
}
Expand Down
75 changes: 6 additions & 69 deletions packages/bridge/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
import { AnalysisOutput } from '../../shared/src/types/analysis.js';
import { CssAnalysisInput } from '../../css/src/analysis/analysis.js';
import { EmbeddedAnalysisInput } from '../../jsts/src/embedded/analysis/analysis.js';
import { JsTsAnalysisInput } from '../../jsts/src/analysis/analysis.js';
import { ProjectAnalysisInput } from '../../jsts/src/analysis/projectAnalysis/projectAnalysis.js';
import { TsConfigJson } from 'type-fest';
import { RuleConfig } from '../../jsts/src/linter/config/rule-config.js';
import { readFile } from '../../shared/src/helpers/files.js';
import { APIError, ErrorCode } from '../../shared/src/errors/error.js';
import { NamedDependency } from '../../jsts/src/rules/index.js';
import { JsTsLanguage, isJsFile, isTsFile } from '../../shared/src/helpers/language.js';
import { CssAnalysisInput } from '../../css/src/analysis/analysis.js';
import { JsTsAnalysisInput } from '../../jsts/src/analysis/analysis.js';
import { EmbeddedAnalysisInput } from '../../jsts/src/embedded/analysis/analysis.js';

export type RequestResult =
| {
Expand All @@ -42,22 +40,6 @@ export type Telemetry = {

export type RequestType = BridgeRequest['type'];

type MaybeIncompleteCssAnalysisInput = Omit<CssAnalysisInput, 'fileContent'> & {
fileContent?: string;
};
type MaybeIncompleteJsTsAnalysisInput = Omit<JsTsAnalysisInput, 'fileContent' | 'language'> & {
fileContent?: string;
language?: JsTsLanguage;
};
type MaybeIncompleteEmbeddedAnalysisInput = Omit<EmbeddedAnalysisInput, 'fileContent'> & {
fileContent?: string;
};

type MaybeIncompleteAnalysisInput =
| MaybeIncompleteJsTsAnalysisInput
| MaybeIncompleteCssAnalysisInput
| MaybeIncompleteEmbeddedAnalysisInput;

export type BridgeRequest =
| CssRequest
| JsTsRequest
Expand All @@ -73,17 +55,17 @@ export type BridgeRequest =

type CssRequest = {
type: 'on-analyze-css';
data: MaybeIncompleteCssAnalysisInput;
data: CssAnalysisInput;
};

type EmbeddedRequest = {
type: 'on-analyze-html' | 'on-analyze-yaml';
data: MaybeIncompleteEmbeddedAnalysisInput;
data: EmbeddedAnalysisInput;
};

type JsTsRequest = {
type: 'on-analyze-jsts';
data: MaybeIncompleteJsTsAnalysisInput;
data: JsTsAnalysisInput;
};

type ProjectAnalysisRequest = {
Expand Down Expand Up @@ -129,51 +111,6 @@ type GetTelemetryRequest = {
type: 'on-get-telemetry';
};

/**
* In SonarQube context, an analysis input includes both path and content of a file
* to analyze. However, in SonarLint, we might only get the file path. As a result,
* we read the file if the content is missing in the input.
*/
export async function readFileLazily<T extends MaybeIncompleteAnalysisInput>(
input: T,
): Promise<T & { fileContent: string }> {
if (!isCompleteAnalysisInput(input)) {
return {
...input,
fileContent: await readFile(input.filePath),
};
}
return input;
}

/**
* In SonarQube context, an analysis input includes both path and content of a file
* to analyze. However, in SonarLint, we might only get the file path. As a result,
* we read the file if the content is missing in the input.
*/
export function fillLanguage<T extends MaybeIncompleteJsTsAnalysisInput & { fileContent: string }>(
input: T,
): JsTsAnalysisInput {
if (isTsFile(input.filePath, input.fileContent)) {
return {
...input,
language: 'ts',
};
} else if (isJsFile(input.filePath)) {
return {
...input,
language: 'js',
};
}
throw new Error(`Unable to find language for file ${input.filePath}`);
}

export function isCompleteAnalysisInput<T extends MaybeIncompleteAnalysisInput>(
input: T,
): input is T & { fileContent: string } {
return 'fileContent' in input;
}

/**
* The default (de)serialization mechanism of the Worker Thread API cannot be used
* to (de)serialize Error instances. To address this, we turn those instances into
Expand Down
5 changes: 3 additions & 2 deletions packages/bridge/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
import * as express from 'express';
import { Worker } from 'worker_threads';
import { createDelegator } from './delegate.js';
import { WorkerData } from '../../shared/src/helpers/worker.js';

export default function (worker?: Worker): express.Router {
export default function (worker: Worker | undefined, workerData?: WorkerData): express.Router {
const router = express.Router();
const delegate = createDelegator(worker);
const delegate = createDelegator(worker, workerData);

/** Endpoints running on the worker thread */
router.post('/analyze-project', delegate('on-analyze-project'));
Expand Down
4 changes: 1 addition & 3 deletions packages/bridge/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
registerGarbageCollectionObserver,
logMemoryConfiguration,
logMemoryError,
logHeapStatistics,
} from './memory.js';

/**
Expand Down Expand Up @@ -84,7 +83,6 @@ export function start(
if (debugMemory) {
registerGarbageCollectionObserver();
}
logHeapStatistics();
return new Promise(resolve => {
debug('Starting the bridge server');

Expand Down Expand Up @@ -113,7 +111,7 @@ export function start(
*/
app.use(express.json({ limit: MAX_REQUEST_SIZE }));
app.use(orphanTimeout.middleware);
app.use(router(worker));
app.use(router(worker, { debugMemory }));
app.use(errorMiddleware);

app.post('/close', (_: express.Request, response: express.Response) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/bridge/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
import { parentPort } from 'worker_threads';
import { parentPort, workerData } from 'worker_threads';
import { handleRequest } from './handle-request.js';
import { BridgeRequest } from './request.js';

Expand All @@ -28,7 +28,7 @@ if (parentPort) {
if (type === 'close') {
parentThread.close();
} else {
parentThread.postMessage(await handleRequest(message));
parentThread.postMessage(await handleRequest(message, workerData));
}
});
}
3 changes: 1 addition & 2 deletions packages/bridge/tests/errors/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
import express from 'express';
import { EMPTY_JSTS_ANALYSIS_OUTPUT, errorMiddleware } from '../../src/errors/index.js';
import { errorMiddleware } from '../../src/errors/index.js';
import assert from 'assert';

import { describe, it, beforeEach, mock, Mock } from 'node:test';
Expand Down Expand Up @@ -47,7 +47,6 @@ describe('errorMiddleware', () => {
line: 42,
code: ErrorCode.Parsing,
},
...EMPTY_JSTS_ANALYSIS_OUTPUT,
},
);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge/tests/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('router', () => {
],
baseDir: fixtures,
files: {
[filePath]: { fileType: 'MAIN' },
[filePath]: { fileType: 'MAIN', filePath },
},
};

Expand Down
Loading

0 comments on commit 2233eec

Please sign in to comment.