Files
OliveTin/frontend/node_modules/stylelint/lib/utils/suppressionsService.cjs

324 lines
8.8 KiB
JavaScript

// 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;