| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- // NOTICE: This file is generated by Rollup. To modify it,
- // please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
- 'use strict';
- const fs = require('node:fs');
- const path = require('node:path');
- const getRelativePath = require('./getRelativePath.cjs');
- const isPathNotFoundError = require('./isPathNotFoundError.cjs');
- /*
- * This file is based on ESLint's suppressions-service.js
- * https://github.com/eslint/eslint/blob/v9.26.0/lib/services/suppressions-service.js
- *
- * Copyright OpenJS Foundation and other contributors, https://openjsf.org/
- * Released under the MIT License:
- * https://github.com/eslint/eslint/blob/main/LICENSE
- */
- /** @import {LintResult, Warning, SuppressedProblems} from 'stylelint' */
- /**
- * Manages the suppressed problems.
- */
- class SuppressionsService {
- filePath = '';
- cwd = '';
- /**
- * Creates a new instance of SuppressionsService.
- * @param {Object} options The options.
- * @param {string} options.filePath The path to the suppressions file.
- * @param {string} options.cwd The current working directory.
- */
- constructor({ filePath, cwd }) {
- this.filePath = filePath;
- this.cwd = cwd;
- }
- /**
- * Updates the suppressions file based on the current problems and the provided rules.
- * If no rules are provided, all problems are suppressed.
- * This method now automatically prunes suppressions that no longer exist.
- * @param {LintResult[] | undefined} results The lint results.
- * @param {string[] | undefined} rules The rules to suppress.
- * @returns {Promise<void>}
- */
- async suppress(results, rules) {
- if (results === undefined) return;
- const suppressions = await this.load();
- for (const result of results) {
- const source = result.source;
- if (!source) continue;
- const relativePath = path.isAbsolute(source) ? getRelativePath(this.cwd, source) : source;
- const problemsByRule = SuppressionsService.countProblemsByRule(result.warnings);
- for (const [rule, ruleData] of problemsByRule) {
- if (rules && !rules.includes(rule)) continue;
- if (!suppressions.has(relativePath)) {
- suppressions.set(relativePath, new Map());
- }
- const fileRules = suppressions.get(relativePath);
- if (!fileRules) continue;
- fileRules.set(rule, ruleData);
- }
- }
- const { unused } = this.applySuppressions(results, suppressions);
- const prunedSuppressions = this.#prune(unused, suppressions);
- return this.#save(prunedSuppressions);
- }
- /**
- * Removes old, unused suppressions for problems that do not occur anymore.
- * @param {SuppressedProblems} unused The unused suppressions.
- * @param {SuppressedProblems} suppressions The suppressions.
- * @returns {SuppressedProblems} The pruned suppressions.
- */
- #prune(unused, suppressions) {
- for (const [file, rules] of unused) {
- if (!suppressions.has(file)) continue;
- for (const [rule, ruleData] of rules) {
- const fileRules = suppressions.get(file);
- if (!fileRules) continue;
- const suppressionData = fileRules.get(rule);
- if (!suppressionData) continue;
- const suppressionsCount = suppressionData.count;
- const problemsCount = ruleData.count;
- if (suppressionsCount === problemsCount) {
- // Remove unused rules
- fileRules.delete(rule);
- } else {
- // Update the count to match the new number of problems
- const ruleDataForUpdate = fileRules.get(rule);
- if (ruleDataForUpdate) {
- ruleDataForUpdate.count -= problemsCount;
- }
- }
- }
- // Cleanup files with no rules
- const fileRulesForCleanup = suppressions.get(file);
- if (fileRulesForCleanup && fileRulesForCleanup.size === 0) {
- suppressions.delete(file);
- }
- }
- return suppressions;
- }
- /**
- * Checks the provided suppressions against the lint results.
- *
- * For each file, counts the number of problems per rule.
- * For each rule in each file, compares the number of problems against the counter from the suppressions file.
- * If the number of problems is less or equal to the counter, warnings are ignored.
- * Otherwise, all problems are reported as usual.
- * @param {LintResult[]} results The lint results.
- * @param {SuppressedProblems} suppressions The suppressions.
- * @returns {{
- * results: LintResult[],
- * unused: SuppressedProblems
- * }} The updated results and the unused suppressions.
- */
- applySuppressions(results, suppressions) {
- /**
- * We copy the results to avoid modifying the original objects
- * We remove only result warnings that are matched and hence suppressed
- * We leave the rest untouched to minimize the risk of losing parts of the original data
- */
- const clonedResults = results.map((r) => {
- return {
- ...r,
- warnings: structuredClone(r.warnings),
- };
- });
- /** @type {SuppressedProblems} */
- const unused = new Map();
- for (const result of clonedResults) {
- const source = result.source;
- if (!source) continue;
- const relativePath = path.isAbsolute(source) ? getRelativePath(this.cwd, source) : source;
- if (!suppressions.has(relativePath)) continue;
- const problemsByRule = SuppressionsService.countProblemsByRule(result.warnings);
- for (const [rule, ruleStats] of problemsByRule) {
- const fileRules = suppressions.get(relativePath);
- if (!fileRules) continue;
- const ruleData = fileRules.get(rule);
- if (!ruleData) continue;
- const suppressionsCount = ruleData.count;
- if (!ruleStats) continue;
- const problemsCount = ruleStats.count;
- // Suppress warnings if the number of problems is less or equal to the suppressions count
- if (problemsCount <= suppressionsCount) {
- result.warnings = result.warnings.filter((warning) => warning.rule !== rule);
- }
- // Update the count to match the new number of problems, otherwise remove the rule entirely
- if (problemsCount < suppressionsCount) {
- if (!unused.has(relativePath)) {
- unused.set(relativePath, new Map());
- }
- const unusedFileRules = unused.get(relativePath);
- if (unusedFileRules && !unusedFileRules.has(rule)) {
- unusedFileRules.set(rule, { count: 0 });
- }
- if (unusedFileRules) {
- const unusedRuleData = unusedFileRules.get(rule);
- if (unusedRuleData) {
- unusedRuleData.count = suppressionsCount - problemsCount;
- }
- }
- }
- }
- // Mark as unused all the suppressions that were not matched against a rule
- const fileRulesForUnused = suppressions.get(relativePath);
- if (fileRulesForUnused) {
- for (const [rule, savedEntry] of fileRulesForUnused) {
- if (problemsByRule.has(rule)) continue;
- if (!savedEntry) continue;
- if (!unused.has(relativePath)) {
- unused.set(relativePath, new Map());
- }
- const unusedFileRulesForSet = unused.get(relativePath);
- if (!unusedFileRulesForSet) continue;
- unusedFileRulesForSet.set(rule, savedEntry);
- }
- }
- }
- return {
- results: clonedResults,
- unused,
- };
- }
- /**
- * Loads the suppressions file.
- * @throws {Error} If the suppressions file cannot be parsed.
- * @returns {Promise<SuppressedProblems>} The suppressions.
- */
- async load() {
- try {
- const data = await fs.promises.readFile(this.filePath, 'utf8');
- const parsed = JSON.parse(data);
- // Convert Object to Map
- const suppressions = new Map();
- for (const [filePath, rules] of Object.entries(parsed)) {
- const rulesMap = new Map();
- for (const [ruleName, ruleData] of Object.entries(rules)) {
- rulesMap.set(ruleName, ruleData);
- }
- suppressions.set(filePath, rulesMap);
- }
- return suppressions;
- } catch (err) {
- if (isPathNotFoundError(err)) {
- return new Map();
- }
- throw new Error(`Failed to parse suppressions file at ${this.filePath}`);
- }
- }
- /**
- * Updates the suppressions file.
- * @param {SuppressedProblems} suppressions The suppressions to save.
- * @returns {Promise<void>}
- */
- #save(suppressions) {
- // Convert Map to Object for JSON serialization
- /** @type {Record<string, Record<string, {count: number}>>} */
- const obj = {};
- for (const [filePath, rulesMap] of suppressions) {
- obj[filePath] = {};
- for (const [ruleName, ruleData] of rulesMap) {
- obj[filePath][ruleName] = ruleData;
- }
- }
- return fs.promises.writeFile(this.filePath, `${JSON.stringify(obj, null, 2)}\n`);
- }
- /**
- * Counts the problems by rule, ignoring warnings.
- * @param {Warning[]} warnings The warnings to count.
- * @returns {Map<string, {count: number}>} The number of problems by rule.
- */
- static countProblemsByRule(warnings) {
- /** @type {Map<string, {count: number}>} */
- const totals = new Map();
- for (const warning of warnings) {
- const rule = warning.rule;
- if (warning.severity !== 'error' || !rule) continue;
- if (!totals.has(rule)) {
- totals.set(rule, { count: 0 });
- }
- const ruleData = totals.get(rule);
- if (ruleData) {
- ruleData.count += 1;
- }
- }
- return totals;
- }
- }
- exports.SuppressionsService = SuppressionsService;
|