UNPKG

eslint-remote-tester

Version:
210 lines (209 loc) 7.55 kB
import fs from 'node:fs'; import { parentPort, workerData } from 'node:worker_threads'; import { resolve } from 'node:path'; import { ESLint } from 'eslint'; import { codeFrameColumns } from '@babel/code-frame'; import config from '../config/index.js'; import { CACHE_LOCATION, getFiles, removeCachedRepository, } from '../file-client/index.js'; // Regex used to attempt parsing out rule which caused linter to crash const RULE_FROM_TRACE_REGEXP = /Rule: "(.*?)"/; const RULE_FROM_LOADING_ERROR = /Error while loading rule '(.*?)'/; const RULE_FROM_PATH_REGEXP = /rules\/(.*?)\.js/; const UNKNOWN_RULE_ID = 'unable-to-parse-rule-id'; // Regex used to attempt parsing out line which caused linter to crash // https://github.com/eslint/eslint/blob/ed1da5d96af2587b7211854e45cf8657ef808710/lib/linter/linter.js#L1194 const LINE_REGEX = /Occurred while linting \S*:([0-9]+)?/; const MAX_ROW_LENGTH = 1000; /** * Create error message for LintMessage results */ export function createErrorMessage(error) { return { line: 0, ...error, column: 0, severity: 2, }; } async function executionTimeWarningWrapper(method, warningMethod, time) { const startTime = process.hrtime(); const results = await method(); const [endTime] = process.hrtime(startTime); if (endTime > time) { warningMethod(endTime); } return results; } /** * Picks out messages which are under testing and constructs a small snippet of * the erroneous code block */ function getMessageReducer(repository) { function messageFilter(message) { // TODO: Unhandled messages from and/or plugins should likely be reported as warnings if (!message.ruleId) return false; if (typeof config.rulesUnderTesting === 'function') { return config.rulesUnderTesting(message.ruleId, { repository }); } return config.rulesUnderTesting.includes(message.ruleId); } return function reducer(all, result) { const messages = result.messages.filter(messageFilter); // Process only rules that are under testing if (messages.length === 0) { return all; } return [ ...all, ...messages.map(message => ({ ...message, source: constructCodeFrame(result.source, message), })), ]; }; } /** * Build code frame from ESLint result, if possible */ function constructCodeFrame(source, message) { if (!source) return undefined; const location = { start: { line: message.line, column: message.column }, }; if (message.endLine != null) { location.end = { line: message.endLine, column: message.endColumn }; } const rows = codeFrameColumns(source, location).split('\n'); const limitedRows = rows.map(row => { if (row.length > MAX_ROW_LENGTH) { return row.slice(0, MAX_ROW_LENGTH - 3) + '...'; } return row; }); return limitedRows.join('\n'); } /** * Parse error stack for erroneous lines and construct `LintMessage` with * source code. */ function parseErrorStack(error, file) { const { path } = file; const stack = error.stack || ''; const ruleMatch = // ESLint v8 stack.match(RULE_FROM_TRACE_REGEXP) || // Older ESLint versions stack.match(RULE_FROM_PATH_REGEXP) || // Rule loading error stack.match(RULE_FROM_LOADING_ERROR) || []; const ruleId = ruleMatch.pop() || UNKNOWN_RULE_ID; const lineMatch = stack.match(LINE_REGEX) || []; const line = parseInt(lineMatch.pop() || '0'); // Include erroneous line to source when line was successfully parsed from the stack let source; if (line > 0) { const content = fs.readFileSync(path, 'utf-8'); source = constructCodeFrame(content, { line, column: 0 }); } return createErrorMessage({ path, line, ruleId, source, error: error.stack, message: error.message, }); } // Wrapper used to enforce WorkerMessage type to parentPort.postMessage calls const postMessage = (message) => { if (parentPort) { return parentPort.postMessage(message); } throw new Error(`parentPort unavailable, message: (${message})`); }; /** * Task for worker threads: * - Expects workerData to contain array of repositories as strings * - Read files from repository-client * - Run ESLint on file contents * - Parse messages and pass lint results back to the main thread * - Keep progress-logger up-to-date of status via onMessage */ export default async function workerTask() { const { repository } = workerData; const messageReducer = getMessageReducer(repository); const files = await getFiles({ repository, onClone: () => postMessage({ type: 'CLONE' }), onCloneFailure: () => postMessage({ type: 'CLONE_FAILURE' }), onPull: () => postMessage({ type: 'PULL' }), onPullFailure: () => postMessage({ type: 'PULL_FAILURE' }), onRead: () => postMessage({ type: 'READ' }), onReadFailure: () => postMessage({ type: 'READ_FAILURE' }), }); const eslintConfig = typeof config.eslintConfig === 'function' ? await config.eslintConfig({ repository, location: resolve(`${CACHE_LOCATION}/${repository}`), }) : config.eslintConfig; const linter = new ESLint({ overrideConfigFile: true, overrideConfig: eslintConfig, // Only rules set in configuration are expected. // Ignore all inline configurations found from target repositories. allowInlineConfig: false, // Lint all given files, ignore none. Cache is located under node_modules. // config.pathIgnorePattern is used for exclusions. ignore: true, ignorePatterns: ['!**/node_modules/'], }); postMessage({ type: 'LINT_START', payload: files.length }); for (const [index, file] of files.entries()) { const fileIndex = index + 1; const { path } = file; let result; try { const lintFile = () => linter.lintFiles(path); if (config.slowLintTimeLimit) { result = await executionTimeWarningWrapper(lintFile, lintTime => postMessage({ type: 'FILE_LINT_SLOW', payload: { path, lintTime }, }), config.slowLintTimeLimit); } else { result = await lintFile(); } } catch (error) { // Catch crashing linter const crashMessage = parseErrorStack(error, file); postMessage({ type: 'ON_RESULT', payload: { messages: [crashMessage] }, }); postMessage({ type: 'FILE_LINT_END', payload: { fileIndex }, }); postMessage({ type: 'LINTER_CRASH', payload: crashMessage.ruleId || '', }); continue; } const messages = result .reduce(messageReducer, []) .filter(Boolean) .map(message => ({ ...message, path })); postMessage({ type: 'ON_RESULT', payload: { messages } }); postMessage({ type: 'FILE_LINT_END', payload: { fileIndex } }); } if (!config.cache) { await removeCachedRepository(repository); } postMessage({ type: 'LINT_END' }); }