All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.plugins.text.checks.BIDICharacterCheck Maven / Gradle / Ivy

/*
 * SonarQube Text Plugin
 * Copyright (C) 2021-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.text.checks;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import org.sonar.check.Rule;
import org.sonar.plugins.common.InputFileContext;
import org.sonar.plugins.text.api.TextCheck;

@Rule(key = "S6389")
public class BIDICharacterCheck extends TextCheck {

  public static final String MESSAGE_FORMAT = "This line contains a bidirectional character in column %d. Make sure that using bidirectional characters is safe here.";

  private static final List BIDI_FORMATTING_CHARS = List.of(
    '\u202A', // Left-To-Right Embedding
    '\u202B', // Right-To-Left Embedding
    '\u202D', // Left-To-Right Override
    '\u202E' // Right-To-Left Override
  );
  private static final List BIDI_ISOLATE_CHARS = List.of(
    '\u2066', // Left-To-Right Isolate
    '\u2067', // Right-To-Left Isolate
    '\u2068' // First Strong Isolate
  );
  private static final List BIDI_CHARS = new ArrayList<>(BIDI_FORMATTING_CHARS);
  static {
    BIDI_CHARS.addAll(BIDI_ISOLATE_CHARS);
  }

  private static final char PDF = '\u202C'; // Pop Directional Formatting
  private static final char PDI = '\u2069'; // Pop Directional Isolate

  @Override
  public void analyze(InputFileContext ctx) {
    List lines = ctx.lines();
    for (var lineOffset = 0; lineOffset < lines.size(); lineOffset++) {
      checkLine(ctx, lines.get(lineOffset), lineOffset + 1);
    }
  }

  private void checkLine(InputFileContext ctx, String lineContent, int lineNumber) {
    for (Character bidiChar : BIDI_CHARS) {
      if (lineContent.indexOf(bidiChar) >= 0) {
        // The line contains at least one BIDI character, let's do a more thorough analysis
        checkLineBIDIChars(ctx, lineContent, lineNumber);
        return;
      }
    }
  }

  /**
   * Look for unclosed BIDI characters. The rules are as follows:
   * - There has to be one closing PDF for every LRE, RLE, LRO, RLO
   * - There has to be one closing PDI for every LRI, RLI, FSI
   */
  private void checkLineBIDIChars(InputFileContext ctx, String lineContent, int lineNumber) {
    Deque unclosedFormattingColumns = new ArrayDeque<>();
    Deque unclosedIsolateColumns = new ArrayDeque<>();

    for (var i = 0; i < lineContent.length(); i++) {
      var currentChar = lineContent.charAt(i);
      if (BIDI_FORMATTING_CHARS.contains(currentChar)) {
        unclosedFormattingColumns.push(i);
      } else if (BIDI_ISOLATE_CHARS.contains(currentChar)) {
        unclosedIsolateColumns.push(i);
      } else if (currentChar == PDF && !unclosedFormattingColumns.isEmpty()) {
        unclosedFormattingColumns.pop();
      } else if (currentChar == PDI && !unclosedIsolateColumns.isEmpty()) {
        unclosedIsolateColumns.pop();
      }
    }

    maybeReportOnFirstColumn(ctx, lineNumber, unclosedFormattingColumns, unclosedIsolateColumns);
  }

  private void maybeReportOnFirstColumn(InputFileContext ctx, int lineNumber, Deque unclosedFormattingColumns,
    Deque unclosedIsolateColumns) {
    if (unclosedFormattingColumns.isEmpty() && unclosedIsolateColumns.isEmpty()) {
      // Everything was closed correctly. Nothing to report.
      return;
    }

    var columnToReport = 0;
    if (!unclosedFormattingColumns.isEmpty() && !unclosedIsolateColumns.isEmpty()) {
      if (unclosedFormattingColumns.getFirst() < unclosedIsolateColumns.getFirst()) {
        columnToReport = unclosedFormattingColumns.getFirst();
      } else {
        columnToReport = unclosedIsolateColumns.getFirst();
      }
    } else if (!unclosedFormattingColumns.isEmpty()) {
      columnToReport = unclosedFormattingColumns.getFirst();
    } else {
      columnToReport = unclosedIsolateColumns.getFirst();
    }

    ctx.reportTextIssue(getRuleKey(), lineNumber, String.format(MESSAGE_FORMAT, columnToReport + 1));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy