
org.sonar.plugins.html.checks.sonar.TableHeaderReferenceCheck Maven / Gradle / Ivy
The newest version!
/*
* SonarQube HTML
* Copyright (C) 2010-2024 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/
*/
package org.sonar.plugins.html.checks.sonar;
import static java.lang.String.format;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.sonar.check.Rule;
import org.sonar.plugins.html.checks.AbstractPageCheck;
import org.sonar.plugins.html.node.Node;
import org.sonar.plugins.html.node.TagNode;
@Rule(key = "S5260")
public class TableHeaderReferenceCheck extends AbstractPageCheck {
private static final Table.Cell NIL = new Table.Cell(null);
private static final Pattern DYNAMIC_HEADERS = Pattern.compile("[{}$()\\[\\]]");
private static final String HEADERS = "HEADERS";
private Deque stack = new LinkedList<>();
@FunctionalInterface
private interface TriFunction {
void apply(A a, B b, C c);
}
private static class Table {
private final List> rows;
private static class Cell {
private final TagNode node;
Cell(TagNode node) {
this.node = node;
}
TagNode node() {
return node;
}
List headers() {
if (node.hasProperty(HEADERS) && !DYNAMIC_HEADERS.matcher(node.getPropertyValue(HEADERS)).find()) {
return Arrays.stream(node.getPropertyValue(HEADERS).split("\\s+")).filter(header -> !header.isEmpty()).toList();
} else {
return Collections.emptyList();
}
}
}
private static class Header extends Cell {
private String id;
Header(TagNode node) {
super(node);
id = node.getPropertyValue("ID");
}
String id() {
return id;
}
}
Table(List> rows) {
this.rows = Collections.unmodifiableList(rows);
}
List> rows() {
return rows;
}
int numberOfCells() {
int max = 0;
for (int i = 0; i < rows.size(); ++i) {
max = Integer.max(max, rows.get(i).size());
}
return max;
}
int numberOfRows() {
return rows.size();
}
void forEachCell(TriFunction action) {
for (int row = 0; row < rows.size(); ++row) {
for (int column = 0; column < rows.get(row).size(); ++column) {
Table.Cell cell = rows.get(row).get(column);
if (cell != NIL) {
action.apply(cell, row, column);
}
}
}
}
List> findHorizontalHeaders() {
List> headers = new ArrayList<>();
for (int i = 0; i < numberOfCells(); ++i) {
headers.add(new HashSet<>());
}
forEachCell((cell, row, column) -> {
if (cell instanceof Table.Header header) {
headers.get(column).add(header.id());
}
});
return headers;
}
List> findVerticalHeaders() {
List> headers = new ArrayList<>();
for (int i = 0; i < numberOfRows(); ++i) {
headers.add(new HashSet<>());
}
forEachCell((cell, row, column) -> {
if (cell instanceof Table.Header header) {
headers.get(row).add(header.id());
}
});
return headers;
}
Map> findReferenceableHeadersPerCellNode() {
List> horizontalHeaders = findHorizontalHeaders();
List> verticalHeaders = findVerticalHeaders();
Map> referenceable = new HashMap<>();
forEachCell((cell, row, column) -> {
if (!cell.headers().isEmpty()) {
List headers = new ArrayList<>();
headers.addAll(horizontalHeaders.get(column));
headers.addAll(verticalHeaders.get(row));
referenceable.merge(cell.node(), headers, (acc, val) -> { acc.addAll(val); return acc; });
}
});
return referenceable;
}
}
private static class TableBuilder {
private ArrayList rows = new ArrayList<>();
private RowBuilder currentRow = null;
private static class RowBuilder {
private List cells = new ArrayList<>();
int indexOfVacantCell() {
for (int i = 0; i < cells.size(); ++i) {
if (cells.get(i) == NIL) {
return i;
}
}
return -1;
}
List build() {
return Collections.unmodifiableList(cells);
}
void set(int cellIndex, Table.Cell cell) {
cells.set(cellIndex, cell);
}
int size() {
return cells.size();
}
void add(Table.Cell cell) {
cells.add(cell);
}
}
void newRow() {
int indexOfCurrentRow = rows.indexOf(currentRow);
if (indexOfCurrentRow == rows.size() - 1) {
currentRow = new RowBuilder();
rows.add(currentRow);
} else {
currentRow = rows.get(rows.indexOf(currentRow) + 1);
}
}
void newCell(Table.Cell cell) {
if (rows.isEmpty()) {
return;
}
int rowspan = getRowSpan(cell.node());
int rowStart = rows.indexOf(currentRow);
int rowEnd = rowStart + rowspan;
int colspan = getColSpan(cell.node());
int cellStart = rows.get(rowStart).indexOfVacantCell();
if (cellStart == -1) {
cellStart = rows.get(rowStart).size();
}
int cellEnd = cellStart + colspan;
for (int row = rowStart; row < rowEnd; ++row) {
if (row == rows.size()) {
rows.add(new RowBuilder());
}
for (int col = cellStart; col < cellEnd; ++col) {
if (col < rows.get(row).size()) {
rows.get(row).set(col, cell);
} else {
for (int i = rows.get(row).size(); i < col; ++i) {
rows.get(row).add(NIL);
}
rows.get(row).add(cell);
}
}
}
}
Table build() {
return new Table(rows.stream().map(RowBuilder::build).toList());
}
private static int getRowSpan(TagNode node) {
String rowspan = node.getPropertyValue("ROWSPAN");
try {
return Integer.parseInt(rowspan);
} catch (NumberFormatException ex) {
return 1;
}
}
private static int getColSpan(TagNode node) {
String rowspan = node.getPropertyValue("COLSPAN");
try {
return Integer.parseInt(rowspan);
} catch (NumberFormatException ex) {
return 1;
}
}
}
@Override
public void startDocument(List nodes) {
stack.clear();
}
@Override
public void startElement(TagNode node) {
if (isTable(node)) {
stack.push(new TableBuilder());
} else if (!stack.isEmpty()) {
if (isTableRow(node)) {
stack.peek().newRow();
} else if (isTableData(node)) {
stack.peek().newCell(new Table.Cell(node));
} else if (isTableHeader(node)) {
stack.peek().newCell(new Table.Header(node));
}
}
}
@Override
public void endElement(TagNode node) {
if (isTable(node) && !stack.isEmpty()) {
raiseViolationOnInvalidReference(stack.pop().build());
}
}
private void raiseViolationOnInvalidReference(Table table) {
Map> referenceableHeaders = table.findReferenceableHeadersPerCellNode();
Map> raisedFor = new HashMap<>();
table.forEachCell((cell, row, column) -> {
TagNode node = cell.node();
List actual = cell.headers();
List expected = referenceableHeaders.getOrDefault(node, Collections.emptyList());
for (String header : actual) {
if (!expected.contains(header) && !raisedFor.getOrDefault(node, Collections.emptyList()).contains(header)) {
if (isExistingHeader(table, header)) {
createViolation(node,
format("id \"%s\" in \"headers\" reference the header of another column/row.", header));
} else {
createViolation(node,
format("id \"%s\" in \"headers\" does not reference any header.", header));
}
raisedFor.merge(node, Arrays.asList(header), (acc, val) -> { acc.addAll(val); return acc; });
break;
}
}
});
}
private static boolean isExistingHeader(Table table, String headerName) {
return table.rows().stream().flatMap(List::stream).filter(cell -> cell instanceof Table.Header)
.map(cell -> (Table.Header) cell).anyMatch(header -> headerName.equalsIgnoreCase(header.id()));
}
private static boolean isTable(TagNode node) {
return node.equalsElementName("TABLE");
}
private static boolean isTableRow(TagNode node) {
return node.equalsElementName("TR");
}
private static boolean isTableData(TagNode node) {
return node.equalsElementName("TD");
}
private static boolean isTableHeader(TagNode node) {
return node.equalsElementName("TH");
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy