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

org.sonar.php.checks.OverwrittenArrayElementCheck Maven / Gradle / Ivy

/*
 * SonarQube PHP Plugin
 * 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 GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * 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 GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.php.checks;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.sonar.check.Rule;
import org.sonar.php.checks.utils.CheckUtils;
import org.sonar.plugins.php.api.symbols.Symbol;
import org.sonar.plugins.php.api.symbols.SymbolTable;
import org.sonar.plugins.php.api.tree.ScriptTree;
import org.sonar.plugins.php.api.tree.Tree;
import org.sonar.plugins.php.api.tree.expression.ArrayAccessTree;
import org.sonar.plugins.php.api.tree.expression.AssignmentExpressionTree;
import org.sonar.plugins.php.api.tree.expression.LiteralTree;
import org.sonar.plugins.php.api.tree.expression.VariableIdentifierTree;
import org.sonar.plugins.php.api.tree.statement.BlockTree;
import org.sonar.plugins.php.api.tree.statement.ExpressionStatementTree;
import org.sonar.plugins.php.api.tree.statement.StatementTree;
import org.sonar.plugins.php.api.visitors.PHPSubscriptionCheck;
import org.sonar.plugins.php.api.visitors.PHPTreeSubscriber;

@Rule(key = "S4143")
public class OverwrittenArrayElementCheck extends PHPSubscriptionCheck {
  private static final String MESSAGE = "Verify this is the array key that was intended to be written to; a value has already been saved for it and not used.";
  private static final String MESSAGE_SECONDARY = "Original assignment.";

  @Override
  public List nodesToVisit() {
    return Arrays.asList(Tree.Kind.BLOCK, Tree.Kind.SCRIPT);
  }

  private Map namesToSymbols = new HashMap<>();
  // >
  private Map> writtenAndUnread = new HashMap<>();

  @Override
  public void visitNode(Tree tree) {
    namesToSymbols.clear();
    writtenAndUnread.clear();

    List statementTrees;
    if (tree.is(Tree.Kind.BLOCK)) {
      statementTrees = ((BlockTree) tree).statements();
    } else {
      statementTrees = ((ScriptTree) tree).statements();
    }

    for (StatementTree statementTree : statementTrees) {
      if (!isArrayKeyAssignmentStatement(statementTree)) {
        removeReadArrayKeys(statementTree);
        continue;
      }

      AssignmentExpressionTree assignmentExpressionTree = (AssignmentExpressionTree) ((ExpressionStatementTree) statementTree).expression();
      ArrayAccessTree arrayAccessTree = (ArrayAccessTree) assignmentExpressionTree.variable();
      String key = ((LiteralTree) arrayAccessTree.offset()).value();
      String variableName = ((VariableIdentifierTree) arrayAccessTree.object()).text();
      Symbol variableSymbol = context().symbolTable().getSymbol(arrayAccessTree.object());

      checkArrayKeyWrite(statementTree, assignmentExpressionTree, key, variableName, variableSymbol);

      updateWrittenAndUnread(assignmentExpressionTree, key, variableName, variableSymbol);
    }

    super.visitNode(tree);
  }

  private void checkArrayKeyWrite(StatementTree statementTree,
    AssignmentExpressionTree assignmentExpressionTree,
    String key,
    String variableName,
    Symbol variableSymbol) {
    if (writtenAndUnread.containsKey(variableName) &&
      writtenAndUnread.get(variableName).containsKey(key) &&
      !symbolWasUsedInTree(variableSymbol, assignmentExpressionTree.value())) {
      AssignmentExpressionTree firstAssignmentTree = writtenAndUnread.get(variableName).get(key);
      context().newIssue(this, statementTree, MESSAGE).secondary(firstAssignmentTree, MESSAGE_SECONDARY);
    }
  }

  private void updateWrittenAndUnread(AssignmentExpressionTree statementTree, String key, String variableName, Symbol variableSymbol) {
    writtenAndUnread.computeIfAbsent(variableName, k -> new HashMap<>()).put(key, statementTree);
    namesToSymbols.put(variableName, variableSymbol);
  }

  private void removeReadArrayKeys(StatementTree statementTree) {
    writtenAndUnread = writtenAndUnread.entrySet().stream()
      .filter(entry -> !symbolWasUsedInTree(namesToSymbols.get(entry.getKey()), statementTree))
      .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
  }

  /**
   * Verifies if a given statement is an array element assignment we want to handle.
   * Currently, only array element assignments that have a depth of one, a literal key, and are not related to predefined
   * global arrays get tracked.
   */
  private static boolean isArrayKeyAssignmentStatement(StatementTree tree) {
    if (!tree.is(Tree.Kind.EXPRESSION_STATEMENT)) {
      return false;
    }

    if (!((ExpressionStatementTree) tree).expression().is(Tree.Kind.ASSIGNMENT)) {
      return false;
    }

    AssignmentExpressionTree assignmentExpressionTree = (AssignmentExpressionTree) ((ExpressionStatementTree) tree).expression();

    if (!assignmentExpressionTree.variable().is(Tree.Kind.ARRAY_ACCESS) ||
      !((ArrayAccessTree) assignmentExpressionTree.variable()).object().is(Tree.Kind.VARIABLE_IDENTIFIER)) {
      return false;
    }

    ArrayAccessTree arrayAccessTree = (ArrayAccessTree) assignmentExpressionTree.variable();
    String arrayName = ((VariableIdentifierTree) arrayAccessTree.object()).text();

    return !CheckUtils.SUPERGLOBALS.contains(arrayName) &&
      ((ArrayAccessTree) assignmentExpressionTree.variable()).offset() != null &&
      ((ArrayAccessTree) assignmentExpressionTree.variable()).offset().is(Tree.Kind.NUMERIC_LITERAL, Tree.Kind.REGULAR_STRING_LITERAL);
  }

  private boolean symbolWasUsedInTree(Symbol symbol, Tree tree) {
    SymbolUsageVisitor checkVisitor = new SymbolUsageVisitor(symbol, context().symbolTable());
    checkVisitor.scanTree(tree);
    return checkVisitor.foundUsage || checkVisitor.foundFlowBreakingStatement;
  }

  /**
   * Given a symbol, the purpose of this visitor is to decide on whether we consider it to have been used in the visited
   * tree. If a usage was found we remove it in the writtenAndUnread map in removeReadArrayKeys().
   *
   * To avoid false positives, we also consider that a symbol was used if we encounter a flow breaking statement.
   */
  private static class SymbolUsageVisitor extends PHPTreeSubscriber {
    private final Symbol symbol;
    private final SymbolTable symbolTable;
    private boolean foundUsage = false;
    private boolean foundFlowBreakingStatement = false;

    public SymbolUsageVisitor(Symbol symbol, SymbolTable symbolTable) {
      this.symbol = symbol;
      this.symbolTable = symbolTable;
    }

    @Override
    public List nodesToVisit() {
      return Arrays.asList(Tree.Kind.VARIABLE_IDENTIFIER,
        Tree.Kind.CONTINUE_STATEMENT,
        Tree.Kind.RETURN_STATEMENT,
        Tree.Kind.BREAK_STATEMENT,
        Tree.Kind.THROW_STATEMENT);
    }

    @Override
    public void visitNode(Tree tree) {
      if (tree.is(Tree.Kind.CONTINUE_STATEMENT, Tree.Kind.RETURN_STATEMENT, Tree.Kind.BREAK_STATEMENT, Tree.Kind.THROW_STATEMENT)) {
        foundFlowBreakingStatement = true;
        return;
      }

      // We have a VARIABLE_IDENTIFIER

      Symbol currentSymbol = symbolTable.getSymbol(tree);

      if (!foundUsage) {
        foundUsage = currentSymbol == symbol;
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy