🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
283 lines
12 KiB
JavaScript
283 lines
12 KiB
JavaScript
"use strict";
|
|
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
|
// See LICENSE in the project root for license information.
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.generatePatchedLinterJsFileIfDoesNotExist = generatePatchedLinterJsFileIfDoesNotExist;
|
|
const fs_1 = __importDefault(require("fs"));
|
|
const constants_1 = require("./constants");
|
|
/**
|
|
* Dynamically generate file to properly patch many versions of ESLint
|
|
* @param inputFilePath - Must be an iteration of https://github.com/eslint/eslint/blob/main/lib/linter/linter.js
|
|
* @param outputFilePath - Some small changes to linter.js
|
|
*/
|
|
function generatePatchedLinterJsFileIfDoesNotExist(inputFilePath, outputFilePath, eslintPackageVersion) {
|
|
const generateEnvVarValue = process.env[constants_1.ESLINT_BULK_FORCE_REGENERATE_PATCH_ENV_VAR_NAME];
|
|
if (generateEnvVarValue !== 'true' && generateEnvVarValue !== '1' && fs_1.default.existsSync(outputFilePath)) {
|
|
return;
|
|
}
|
|
const majorVersion = parseInt(eslintPackageVersion, 10);
|
|
const inputFile = fs_1.default.readFileSync(inputFilePath).toString();
|
|
let inputIndex = 0;
|
|
/**
|
|
* Extract from the stream until marker is reached. When matching marker,
|
|
* ignore whitespace in the stream and in the marker. Return the extracted text.
|
|
*/
|
|
function scanUntilMarker(marker) {
|
|
const trimmedMarker = marker.replace(/\s/g, '');
|
|
let output = '';
|
|
let trimmed = '';
|
|
while (inputIndex < inputFile.length) {
|
|
const char = inputFile[inputIndex++];
|
|
output += char;
|
|
if (!/^\s$/.test(char)) {
|
|
trimmed += char;
|
|
}
|
|
if (trimmed.endsWith(trimmedMarker)) {
|
|
return output;
|
|
}
|
|
}
|
|
throw new Error('Unexpected end of input while looking for ' + JSON.stringify(marker));
|
|
}
|
|
function scanUntilNewline() {
|
|
let output = '';
|
|
while (inputIndex < inputFile.length) {
|
|
const char = inputFile[inputIndex++];
|
|
output += char;
|
|
if (char === '\n') {
|
|
return output;
|
|
}
|
|
}
|
|
throw new Error('Unexpected end of input while looking for new line');
|
|
}
|
|
function scanUntilEnd() {
|
|
const output = inputFile.substring(inputIndex);
|
|
inputIndex = inputFile.length;
|
|
return output;
|
|
}
|
|
const markerForStartOfClassMethodSpaces = '\n */\n ';
|
|
const markerForStartOfClassMethodTabs = '\n\t */\n\t';
|
|
function indexOfStartOfClassMethod(input, position) {
|
|
let startOfClassMethodIndex = input.indexOf(markerForStartOfClassMethodSpaces, position);
|
|
if (startOfClassMethodIndex === -1) {
|
|
startOfClassMethodIndex = input.indexOf(markerForStartOfClassMethodTabs, position);
|
|
if (startOfClassMethodIndex === -1) {
|
|
return { index: startOfClassMethodIndex };
|
|
}
|
|
return { index: startOfClassMethodIndex, marker: markerForStartOfClassMethodTabs };
|
|
}
|
|
return { index: startOfClassMethodIndex, marker: markerForStartOfClassMethodSpaces };
|
|
}
|
|
/**
|
|
* Returns index of next public method
|
|
* @param fromIndex - index of inputFile to search if public method still exists
|
|
* @returns -1 if public method does not exist or index of next public method
|
|
*/
|
|
function getIndexOfNextMethod(fromIndex) {
|
|
const rest = inputFile.substring(fromIndex);
|
|
const endOfClassIndex = rest.indexOf('\n}');
|
|
const { index: startOfClassMethodIndex, marker: startOfClassMethodMarker } = indexOfStartOfClassMethod(rest);
|
|
if (startOfClassMethodIndex === -1 ||
|
|
!startOfClassMethodMarker ||
|
|
startOfClassMethodIndex > endOfClassIndex) {
|
|
return { index: -1 };
|
|
}
|
|
const afterMarkerIndex = startOfClassMethodIndex + startOfClassMethodMarker.length;
|
|
const isPublicMethod = rest[afterMarkerIndex] !== '_' &&
|
|
rest[afterMarkerIndex] !== '#' &&
|
|
!rest.substring(afterMarkerIndex, rest.indexOf('\n', afterMarkerIndex)).includes('static') &&
|
|
!rest.substring(afterMarkerIndex, rest.indexOf('\n', afterMarkerIndex)).includes('constructor');
|
|
return { index: fromIndex + afterMarkerIndex, isPublic: isPublicMethod };
|
|
}
|
|
function scanUntilIndex(indexToScanTo) {
|
|
const output = inputFile.substring(inputIndex, indexToScanTo);
|
|
inputIndex = indexToScanTo;
|
|
return output;
|
|
}
|
|
let outputFile = '';
|
|
// Match this:
|
|
// //------------------------------------------------------------------------------
|
|
// // Requirements
|
|
// //------------------------------------------------------------------------------
|
|
outputFile += scanUntilMarker('// Requirements');
|
|
outputFile += scanUntilMarker('//--');
|
|
outputFile += scanUntilNewline();
|
|
outputFile += `
|
|
// --- BEGIN MONKEY PATCH ---
|
|
const bulkSuppressionsPatch = require(process.env.${constants_1.ESLINT_BULK_PATCH_PATH_ENV_VAR_NAME});
|
|
const requireFromPathToLinterJS = bulkSuppressionsPatch.requireFromPathToLinterJS;
|
|
`;
|
|
// Match this:
|
|
// //------------------------------------------------------------------------------
|
|
// // Typedefs
|
|
// //------------------------------------------------------------------------------
|
|
const requireSection = scanUntilMarker('// Typedefs');
|
|
// Match something like this:
|
|
//
|
|
// const path = require('path'),
|
|
// eslintScope = require('eslint-scope'),
|
|
// evk = require('eslint-visitor-keys'),
|
|
//
|
|
// Convert to something like this:
|
|
//
|
|
// const path = require('path'),
|
|
// eslintScope = requireFromPathToLinterJS('eslint-scope'),
|
|
// evk = requireFromPathToLinterJS('eslint-visitor-keys'),
|
|
//
|
|
outputFile += requireSection.replace(/require\s*\((?:'([^']+)'|"([^"]+)")\)/g, (match, p1, p2) => {
|
|
var _a;
|
|
const importPath = (_a = p1 !== null && p1 !== void 0 ? p1 : p2) !== null && _a !== void 0 ? _a : '';
|
|
if (importPath !== 'path') {
|
|
if (p1) {
|
|
return `requireFromPathToLinterJS('${p1}')`;
|
|
}
|
|
if (p2) {
|
|
return `requireFromPathToLinterJS("${p2}")`;
|
|
}
|
|
}
|
|
// Keep as-is
|
|
return match;
|
|
});
|
|
outputFile += `--- END MONKEY PATCH ---
|
|
`;
|
|
if (majorVersion >= 9) {
|
|
outputFile += scanUntilMarker('const emitter = createEmitter();');
|
|
outputFile += `
|
|
// --- BEGIN MONKEY PATCH ---
|
|
let currentNode = undefined;
|
|
// --- END MONKEY PATCH ---`;
|
|
}
|
|
// Match this:
|
|
// ```
|
|
// if (reportTranslator === null) {
|
|
// reportTranslator = createReportTranslator({
|
|
// ruleId,
|
|
// severity,
|
|
// sourceCode,
|
|
// messageIds,
|
|
// disableFixes
|
|
// });
|
|
// }
|
|
// const problem = reportTranslator(...args);
|
|
//
|
|
// if (problem.fix && !(rule.meta && rule.meta.fixable)) {
|
|
// throw new Error("Fixable rules must set the `meta.fixable` property to \"code\" or \"whitespace\".");
|
|
// }
|
|
// ```
|
|
//
|
|
// Convert to something like this:
|
|
// ```
|
|
// if (reportTranslator === null) {
|
|
// reportTranslator = createReportTranslator({
|
|
// ruleId,
|
|
// severity,
|
|
// sourceCode,
|
|
// messageIds,
|
|
// disableFixes
|
|
// });
|
|
// }
|
|
// const problem = reportTranslator(...args);
|
|
// // --- BEGIN MONKEY PATCH ---
|
|
// if (bulkSuppressionsPatch.shouldBulkSuppress({ filename, currentNode: args[0]?.node ?? currentNode, ruleId, problem })) return;
|
|
// // --- END MONKEY PATCH ---
|
|
//
|
|
// if (problem.fix && !(rule.meta && rule.meta.fixable)) {
|
|
// throw new Error("Fixable rules must set the `meta.fixable` property to \"code\" or \"whitespace\".");
|
|
// }
|
|
// ```
|
|
outputFile += scanUntilMarker('const problem = reportTranslator(...args);');
|
|
outputFile += `
|
|
// --- BEGIN MONKEY PATCH ---
|
|
if (bulkSuppressionsPatch.shouldBulkSuppress({ filename, currentNode: args[0]?.node ?? currentNode, ruleId, problem })) return;
|
|
// --- END MONKEY PATCH ---`;
|
|
//
|
|
// Match this:
|
|
// ```
|
|
// Object.keys(ruleListeners).forEach(selector => {
|
|
// ...
|
|
// });
|
|
// ```
|
|
//
|
|
// Convert to something like this:
|
|
// ```
|
|
// Object.keys(ruleListeners).forEach(selector => {
|
|
// // --- BEGIN MONKEY PATCH ---
|
|
// emitter.on(selector, (...args) => { currentNode = args[args.length - 1]; });
|
|
// // --- END MONKEY PATCH ---
|
|
// ...
|
|
// });
|
|
// ```
|
|
if (majorVersion >= 9) {
|
|
outputFile += scanUntilMarker('Object.keys(ruleListeners).forEach(selector => {');
|
|
outputFile += `
|
|
// --- BEGIN MONKEY PATCH ---
|
|
emitter.on(selector, (...args) => { currentNode = args[args.length - 1]; });
|
|
// --- END MONKEY PATCH ---`;
|
|
}
|
|
outputFile += scanUntilMarker('class Linter {');
|
|
outputFile += scanUntilNewline();
|
|
outputFile += `
|
|
// --- BEGIN MONKEY PATCH ---
|
|
/**
|
|
* We intercept ESLint execution at the .eslintrc.js file, but unfortunately the Linter class is
|
|
* initialized before the .eslintrc.js file is executed. This means the internalSlotsMap that all
|
|
* the patched methods refer to is not initialized. This method checks if the internalSlotsMap is
|
|
* initialized, and if not, initializes it.
|
|
*/
|
|
_conditionallyReinitialize({ cwd, configType } = {}) {
|
|
if (internalSlotsMap.get(this) === undefined) {
|
|
internalSlotsMap.set(this, {
|
|
cwd: normalizeCwd(cwd),
|
|
lastConfigArray: null,
|
|
lastSourceCode: null,
|
|
lastSuppressedMessages: [],
|
|
configType, // TODO: Remove after flat config conversion
|
|
parserMap: new Map([['espree', espree]]),
|
|
ruleMap: new Rules()
|
|
});
|
|
|
|
this.version = pkg.version;
|
|
}
|
|
}
|
|
// --- END MONKEY PATCH ---
|
|
`;
|
|
const privateMethodNames = [];
|
|
let { index: indexOfNextMethod, isPublic } = getIndexOfNextMethod(inputIndex);
|
|
while (indexOfNextMethod !== -1) {
|
|
outputFile += scanUntilIndex(indexOfNextMethod);
|
|
if (isPublic) {
|
|
// Inject the monkey patch at the start of the public method
|
|
outputFile += scanUntilNewline();
|
|
outputFile += ` // --- BEGIN MONKEY PATCH ---
|
|
this._conditionallyReinitialize();
|
|
// --- END MONKEY PATCH ---
|
|
`;
|
|
}
|
|
else if (inputFile[inputIndex] === '#') {
|
|
// Replace the '#' private method with a '_' private method, so that our monkey patch
|
|
// can still call it. Otherwise, we get the following error during execution:
|
|
// TypeError: Receiver must be an instance of class Linter
|
|
const privateMethodName = scanUntilMarker('(');
|
|
// Remove the '(' at the end and stash it, since we need to escape it for the regex later
|
|
privateMethodNames.push(privateMethodName.slice(0, -1));
|
|
outputFile += `_${privateMethodName.slice(1)}`;
|
|
}
|
|
const indexResult = getIndexOfNextMethod(inputIndex);
|
|
indexOfNextMethod = indexResult.index;
|
|
isPublic = indexResult.isPublic;
|
|
}
|
|
outputFile += scanUntilEnd();
|
|
// Do a second pass to find and replace all calls to private methods with the patched versions.
|
|
if (privateMethodNames.length) {
|
|
// eslint-disable-next-line @rushstack/security/no-unsafe-regexp
|
|
const privateMethodCallRegex = new RegExp(`\.(${privateMethodNames.join('|')})\\(`, 'g');
|
|
outputFile = outputFile.replace(privateMethodCallRegex, (match, privateMethodName) => {
|
|
// Replace the leading '#' with a leading '_'
|
|
return `._${privateMethodName.slice(1)}(`;
|
|
});
|
|
}
|
|
fs_1.default.writeFileSync(outputFilePath, outputFile);
|
|
}
|
|
//# sourceMappingURL=generate-patched-file.js.map
|