generate-patched-file.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. "use strict";
  2. // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
  3. // See LICENSE in the project root for license information.
  4. var __importDefault = (this && this.__importDefault) || function (mod) {
  5. return (mod && mod.__esModule) ? mod : { "default": mod };
  6. };
  7. Object.defineProperty(exports, "__esModule", { value: true });
  8. exports.generatePatchedLinterJsFileIfDoesNotExist = generatePatchedLinterJsFileIfDoesNotExist;
  9. const fs_1 = __importDefault(require("fs"));
  10. const constants_1 = require("./constants");
  11. /**
  12. * Dynamically generate file to properly patch many versions of ESLint
  13. * @param inputFilePath - Must be an iteration of https://github.com/eslint/eslint/blob/main/lib/linter/linter.js
  14. * @param outputFilePath - Some small changes to linter.js
  15. */
  16. function generatePatchedLinterJsFileIfDoesNotExist(inputFilePath, outputFilePath, eslintPackageVersion) {
  17. const generateEnvVarValue = process.env[constants_1.ESLINT_BULK_FORCE_REGENERATE_PATCH_ENV_VAR_NAME];
  18. if (generateEnvVarValue !== 'true' && generateEnvVarValue !== '1' && fs_1.default.existsSync(outputFilePath)) {
  19. return;
  20. }
  21. const majorVersion = parseInt(eslintPackageVersion, 10);
  22. const inputFile = fs_1.default.readFileSync(inputFilePath).toString();
  23. let inputIndex = 0;
  24. /**
  25. * Extract from the stream until marker is reached. When matching marker,
  26. * ignore whitespace in the stream and in the marker. Return the extracted text.
  27. */
  28. function scanUntilMarker(marker) {
  29. const trimmedMarker = marker.replace(/\s/g, '');
  30. let output = '';
  31. let trimmed = '';
  32. while (inputIndex < inputFile.length) {
  33. const char = inputFile[inputIndex++];
  34. output += char;
  35. if (!/^\s$/.test(char)) {
  36. trimmed += char;
  37. }
  38. if (trimmed.endsWith(trimmedMarker)) {
  39. return output;
  40. }
  41. }
  42. throw new Error('Unexpected end of input while looking for ' + JSON.stringify(marker));
  43. }
  44. function scanUntilNewline() {
  45. let output = '';
  46. while (inputIndex < inputFile.length) {
  47. const char = inputFile[inputIndex++];
  48. output += char;
  49. if (char === '\n') {
  50. return output;
  51. }
  52. }
  53. throw new Error('Unexpected end of input while looking for new line');
  54. }
  55. function scanUntilEnd() {
  56. const output = inputFile.substring(inputIndex);
  57. inputIndex = inputFile.length;
  58. return output;
  59. }
  60. const markerForStartOfClassMethodSpaces = '\n */\n ';
  61. const markerForStartOfClassMethodTabs = '\n\t */\n\t';
  62. function indexOfStartOfClassMethod(input, position) {
  63. let startOfClassMethodIndex = input.indexOf(markerForStartOfClassMethodSpaces, position);
  64. if (startOfClassMethodIndex === -1) {
  65. startOfClassMethodIndex = input.indexOf(markerForStartOfClassMethodTabs, position);
  66. if (startOfClassMethodIndex === -1) {
  67. return { index: startOfClassMethodIndex };
  68. }
  69. return { index: startOfClassMethodIndex, marker: markerForStartOfClassMethodTabs };
  70. }
  71. return { index: startOfClassMethodIndex, marker: markerForStartOfClassMethodSpaces };
  72. }
  73. /**
  74. * Returns index of next public method
  75. * @param fromIndex - index of inputFile to search if public method still exists
  76. * @returns -1 if public method does not exist or index of next public method
  77. */
  78. function getIndexOfNextMethod(fromIndex) {
  79. const rest = inputFile.substring(fromIndex);
  80. const endOfClassIndex = rest.indexOf('\n}');
  81. const { index: startOfClassMethodIndex, marker: startOfClassMethodMarker } = indexOfStartOfClassMethod(rest);
  82. if (startOfClassMethodIndex === -1 ||
  83. !startOfClassMethodMarker ||
  84. startOfClassMethodIndex > endOfClassIndex) {
  85. return { index: -1 };
  86. }
  87. const afterMarkerIndex = startOfClassMethodIndex + startOfClassMethodMarker.length;
  88. const isPublicMethod = rest[afterMarkerIndex] !== '_' &&
  89. rest[afterMarkerIndex] !== '#' &&
  90. !rest.substring(afterMarkerIndex, rest.indexOf('\n', afterMarkerIndex)).includes('static') &&
  91. !rest.substring(afterMarkerIndex, rest.indexOf('\n', afterMarkerIndex)).includes('constructor');
  92. return { index: fromIndex + afterMarkerIndex, isPublic: isPublicMethod };
  93. }
  94. function scanUntilIndex(indexToScanTo) {
  95. const output = inputFile.substring(inputIndex, indexToScanTo);
  96. inputIndex = indexToScanTo;
  97. return output;
  98. }
  99. let outputFile = '';
  100. // Match this:
  101. // //------------------------------------------------------------------------------
  102. // // Requirements
  103. // //------------------------------------------------------------------------------
  104. outputFile += scanUntilMarker('// Requirements');
  105. outputFile += scanUntilMarker('//--');
  106. outputFile += scanUntilNewline();
  107. outputFile += `
  108. // --- BEGIN MONKEY PATCH ---
  109. const bulkSuppressionsPatch = require(process.env.${constants_1.ESLINT_BULK_PATCH_PATH_ENV_VAR_NAME});
  110. const requireFromPathToLinterJS = bulkSuppressionsPatch.requireFromPathToLinterJS;
  111. `;
  112. // Match this:
  113. // //------------------------------------------------------------------------------
  114. // // Typedefs
  115. // //------------------------------------------------------------------------------
  116. const requireSection = scanUntilMarker('// Typedefs');
  117. // Match something like this:
  118. //
  119. // const path = require('path'),
  120. // eslintScope = require('eslint-scope'),
  121. // evk = require('eslint-visitor-keys'),
  122. //
  123. // Convert to something like this:
  124. //
  125. // const path = require('path'),
  126. // eslintScope = requireFromPathToLinterJS('eslint-scope'),
  127. // evk = requireFromPathToLinterJS('eslint-visitor-keys'),
  128. //
  129. outputFile += requireSection.replace(/require\s*\((?:'([^']+)'|"([^"]+)")\)/g, (match, p1, p2) => {
  130. var _a;
  131. const importPath = (_a = p1 !== null && p1 !== void 0 ? p1 : p2) !== null && _a !== void 0 ? _a : '';
  132. if (importPath !== 'path') {
  133. if (p1) {
  134. return `requireFromPathToLinterJS('${p1}')`;
  135. }
  136. if (p2) {
  137. return `requireFromPathToLinterJS("${p2}")`;
  138. }
  139. }
  140. // Keep as-is
  141. return match;
  142. });
  143. outputFile += `--- END MONKEY PATCH ---
  144. `;
  145. if (majorVersion >= 9) {
  146. outputFile += scanUntilMarker('const emitter = createEmitter();');
  147. outputFile += `
  148. // --- BEGIN MONKEY PATCH ---
  149. let currentNode = undefined;
  150. // --- END MONKEY PATCH ---`;
  151. }
  152. // Match this:
  153. // ```
  154. // if (reportTranslator === null) {
  155. // reportTranslator = createReportTranslator({
  156. // ruleId,
  157. // severity,
  158. // sourceCode,
  159. // messageIds,
  160. // disableFixes
  161. // });
  162. // }
  163. // const problem = reportTranslator(...args);
  164. //
  165. // if (problem.fix && !(rule.meta && rule.meta.fixable)) {
  166. // throw new Error("Fixable rules must set the `meta.fixable` property to \"code\" or \"whitespace\".");
  167. // }
  168. // ```
  169. //
  170. // Convert to something like this:
  171. // ```
  172. // if (reportTranslator === null) {
  173. // reportTranslator = createReportTranslator({
  174. // ruleId,
  175. // severity,
  176. // sourceCode,
  177. // messageIds,
  178. // disableFixes
  179. // });
  180. // }
  181. // const problem = reportTranslator(...args);
  182. // // --- BEGIN MONKEY PATCH ---
  183. // if (bulkSuppressionsPatch.shouldBulkSuppress({ filename, currentNode: args[0]?.node ?? currentNode, ruleId, problem })) return;
  184. // // --- END MONKEY PATCH ---
  185. //
  186. // if (problem.fix && !(rule.meta && rule.meta.fixable)) {
  187. // throw new Error("Fixable rules must set the `meta.fixable` property to \"code\" or \"whitespace\".");
  188. // }
  189. // ```
  190. outputFile += scanUntilMarker('const problem = reportTranslator(...args);');
  191. outputFile += `
  192. // --- BEGIN MONKEY PATCH ---
  193. if (bulkSuppressionsPatch.shouldBulkSuppress({ filename, currentNode: args[0]?.node ?? currentNode, ruleId, problem })) return;
  194. // --- END MONKEY PATCH ---`;
  195. //
  196. // Match this:
  197. // ```
  198. // Object.keys(ruleListeners).forEach(selector => {
  199. // ...
  200. // });
  201. // ```
  202. //
  203. // Convert to something like this:
  204. // ```
  205. // Object.keys(ruleListeners).forEach(selector => {
  206. // // --- BEGIN MONKEY PATCH ---
  207. // emitter.on(selector, (...args) => { currentNode = args[args.length - 1]; });
  208. // // --- END MONKEY PATCH ---
  209. // ...
  210. // });
  211. // ```
  212. if (majorVersion >= 9) {
  213. outputFile += scanUntilMarker('Object.keys(ruleListeners).forEach(selector => {');
  214. outputFile += `
  215. // --- BEGIN MONKEY PATCH ---
  216. emitter.on(selector, (...args) => { currentNode = args[args.length - 1]; });
  217. // --- END MONKEY PATCH ---`;
  218. }
  219. outputFile += scanUntilMarker('class Linter {');
  220. outputFile += scanUntilNewline();
  221. outputFile += `
  222. // --- BEGIN MONKEY PATCH ---
  223. /**
  224. * We intercept ESLint execution at the .eslintrc.js file, but unfortunately the Linter class is
  225. * initialized before the .eslintrc.js file is executed. This means the internalSlotsMap that all
  226. * the patched methods refer to is not initialized. This method checks if the internalSlotsMap is
  227. * initialized, and if not, initializes it.
  228. */
  229. _conditionallyReinitialize({ cwd, configType } = {}) {
  230. if (internalSlotsMap.get(this) === undefined) {
  231. internalSlotsMap.set(this, {
  232. cwd: normalizeCwd(cwd),
  233. lastConfigArray: null,
  234. lastSourceCode: null,
  235. lastSuppressedMessages: [],
  236. configType, // TODO: Remove after flat config conversion
  237. parserMap: new Map([['espree', espree]]),
  238. ruleMap: new Rules()
  239. });
  240. this.version = pkg.version;
  241. }
  242. }
  243. // --- END MONKEY PATCH ---
  244. `;
  245. const privateMethodNames = [];
  246. let { index: indexOfNextMethod, isPublic } = getIndexOfNextMethod(inputIndex);
  247. while (indexOfNextMethod !== -1) {
  248. outputFile += scanUntilIndex(indexOfNextMethod);
  249. if (isPublic) {
  250. // Inject the monkey patch at the start of the public method
  251. outputFile += scanUntilNewline();
  252. outputFile += ` // --- BEGIN MONKEY PATCH ---
  253. this._conditionallyReinitialize();
  254. // --- END MONKEY PATCH ---
  255. `;
  256. }
  257. else if (inputFile[inputIndex] === '#') {
  258. // Replace the '#' private method with a '_' private method, so that our monkey patch
  259. // can still call it. Otherwise, we get the following error during execution:
  260. // TypeError: Receiver must be an instance of class Linter
  261. const privateMethodName = scanUntilMarker('(');
  262. // Remove the '(' at the end and stash it, since we need to escape it for the regex later
  263. privateMethodNames.push(privateMethodName.slice(0, -1));
  264. outputFile += `_${privateMethodName.slice(1)}`;
  265. }
  266. const indexResult = getIndexOfNextMethod(inputIndex);
  267. indexOfNextMethod = indexResult.index;
  268. isPublic = indexResult.isPublic;
  269. }
  270. outputFile += scanUntilEnd();
  271. // Do a second pass to find and replace all calls to private methods with the patched versions.
  272. if (privateMethodNames.length) {
  273. // eslint-disable-next-line @rushstack/security/no-unsafe-regexp
  274. const privateMethodCallRegex = new RegExp(`\.(${privateMethodNames.join('|')})\\(`, 'g');
  275. outputFile = outputFile.replace(privateMethodCallRegex, (match, privateMethodName) => {
  276. // Replace the leading '#' with a leading '_'
  277. return `._${privateMethodName.slice(1)}(`;
  278. });
  279. }
  280. fs_1.default.writeFileSync(outputFilePath, outputFile);
  281. }
  282. //# sourceMappingURL=generate-patched-file.js.map