eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
123 lines (122 loc) • 5.26 kB
JavaScript
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
// https://sonarsource.github.io/rspec/#/rspec/S5260/javascript
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const table_js_1 = require("../helpers/table.js");
const index_js_1 = require("../helpers/index.js");
const meta_js_1 = require("./meta.js");
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta),
create(context) {
const verifyHeaderReferences = (tree) => {
const grid = (0, table_js_1.computeGrid)(context, tree);
if (grid === null || grid.length === 0) {
// Unknown table structures as well as empty tables should be considered valid
return;
}
const rowHeaders = Array.from({ length: grid.length }, (_, idx) => {
const ids = grid[idx]
.filter(({ isHeader, id }) => isHeader && id)
.map(({ id }) => id);
return new Set(ids);
});
const colHeaders = Array.from({ length: grid[0].length }, (_, idx) => {
const ids = grid
.map(row => row[idx])
.filter(cell => cell)
.filter(({ isHeader, id }) => isHeader && id)
.map(({ id }) => id);
return new Set(ids);
});
const allHeaders = new Set([
...rowHeaders.reduce((headers, acc) => new Set([...headers, ...acc]), new Set()),
...colHeaders.reduce((headers, acc) => new Set([...headers, ...acc]), new Set()),
]);
const internalNodeToPositions = compileBlockInfo(grid);
for (const { minRow, maxRow, minCol, maxCol, cell } of internalNodeToPositions.values()) {
if (!cell.headers || cell.headers.length === 0) {
continue;
}
const actualHeaders = [
...colHeaders.slice(minCol, maxCol + 1),
...rowHeaders.slice(minRow, maxRow + 1),
].reduce((headers, acc) => new Set([...headers, ...acc]), new Set());
for (const header of cell.headers) {
if (!actualHeaders.has(header)) {
if (allHeaders.has(header)) {
context.report({
node: cell.node,
message: `id "${header}" in "headers" references the header of another column/row.`,
});
}
else {
context.report({
node: cell.node,
message: `id "${header}" in "headers" does not reference any <th> header.`,
});
}
}
}
}
};
return {
JSXElement(node) {
const tree = node;
const elementType = (0, index_js_1.getElementType)(context)(tree.openingElement);
if (elementType === 'table') {
verifyHeaderReferences(tree);
}
},
};
},
};
/**
* Extracts an alternative representation of the blocks making up the table. Takes into account that a single block can
* span more than just a 1x1 cell thanks to the "rowspan" and "colspan" attributes. Each block is assigned an internal
* number during computation. Afterward, for each block, we compute its position in the resulting table.
*/
function compileBlockInfo(grid) {
const internalNodeToPositions = new Map();
for (let row = 0; row < grid.length; row++) {
for (let col = 0; col < grid[row].length; col++) {
const cell = grid[row][col];
if (!cell.headers) {
continue;
}
const oldValue = internalNodeToPositions.get(cell.internalNodeId);
if (oldValue !== undefined) {
internalNodeToPositions.set(cell.internalNodeId, {
...oldValue,
maxRow: row,
maxCol: col,
});
}
else {
internalNodeToPositions.set(cell.internalNodeId, {
minRow: row,
maxRow: row,
minCol: col,
maxCol: col,
cell,
});
}
}
}
return internalNodeToPositions;
}
;