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

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

The newest version!
/*
 * 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.ArrayDeque;
import java.util.Comparator;
import java.util.Deque;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.sonar.check.Rule;
import org.sonar.php.checks.utils.CheckUtils;
import org.sonar.php.tree.TreeUtils;
import org.sonar.php.tree.impl.PHPTree;
import org.sonar.plugins.php.api.cfg.CfgBlock;
import org.sonar.plugins.php.api.cfg.CfgBranchingBlock;
import org.sonar.plugins.php.api.cfg.ControlFlowGraph;
import org.sonar.plugins.php.api.tree.Tree;
import org.sonar.plugins.php.api.tree.Tree.Kind;
import org.sonar.plugins.php.api.tree.declaration.FunctionDeclarationTree;
import org.sonar.plugins.php.api.tree.declaration.FunctionTree;
import org.sonar.plugins.php.api.tree.declaration.MethodDeclarationTree;
import org.sonar.plugins.php.api.tree.declaration.VariableDeclarationTree;
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.FunctionCallTree;
import org.sonar.plugins.php.api.tree.expression.FunctionExpressionTree;
import org.sonar.plugins.php.api.tree.expression.MemberAccessTree;
import org.sonar.plugins.php.api.tree.expression.VariableIdentifierTree;
import org.sonar.plugins.php.api.tree.expression.VariableTree;
import org.sonar.plugins.php.api.tree.statement.CatchBlockTree;
import org.sonar.plugins.php.api.tree.statement.ForEachStatementTree;
import org.sonar.plugins.php.api.visitors.PHPVisitorCheck;

@Rule(key = "S836")
public class UseOfUninitializedVariableCheck extends PHPVisitorCheck {
  private static final String MESSAGE = "Review the data-flow - use of uninitialized value.";

  private static final Set PARENT_INITIALIZATION_KIND = EnumSet.of(
    // Note: LEXICAL_VARIABLES are both, read and write, see VariableVisitor#visitFunctionExpression
    Kind.PARAMETER,
    Kind.GLOBAL_STATEMENT,
    Kind.VARIABLE_DECLARATION,
    Kind.REFERENCE_VARIABLE,
    Kind.ARRAY_ASSIGNMENT_PATTERN_ELEMENT,
    Kind.UNSET_VARIABLE_STATEMENT,
    // CatchBlockTree#variable
    Kind.CATCH_BLOCK,
    Kind.ASSIGNMENT_BY_REFERENCE);

  private static final Set FUNCTION_CHANGING_CURRENT_SCOPE = Set.of(
    "eval",
    "extract",
    "parse_str",
    // PREG_REPLACE_EVAL option was deprecated in php 5.5 and has been removed in php 7.0
    "preg_replace",
    "include",
    "include_once",
    "require",
    "require_once");

  // Note: "$argc" and "$argv" are not available in the function scope without using "global"
  private static final Set PREDEFINED_VARIABLES = Set.of(
    "$_COOKIE",
    "$_ENV",
    "$_FILES",
    "$_GET",
    "$_POST",
    "$_REQUEST",
    "$_SERVER",
    "$_SESSION",
    "$GLOBALS",
    "$HTTP_RAW_POST_DATA",
    "$HTTP_RESPONSE_HEADER",
    "$PHP_ERRORMSG",
    // "$this" is defined only in method, but rule S2014 raises issues when it's used elsewhere
    "$THIS");

  private static final Set FUNCTION_ALLOWING_ARGUMENT_CHECK;

  static {
    FUNCTION_ALLOWING_ARGUMENT_CHECK = new HashSet<>(IgnoredReturnValueCheck.PURE_FUNCTIONS);
    FUNCTION_ALLOWING_ARGUMENT_CHECK.add("echo");
    FUNCTION_ALLOWING_ARGUMENT_CHECK.remove("isset");
  }

  @Override
  public void visitFunctionDeclaration(FunctionDeclarationTree tree) {
    checkFunction(tree);
    super.visitFunctionDeclaration(tree);
  }

  @Override
  public void visitMethodDeclaration(MethodDeclarationTree tree) {
    checkFunction(tree);
    super.visitMethodDeclaration(tree);
  }

  @Override
  public void visitFunctionExpression(FunctionExpressionTree tree) {
    checkFunction(tree);
    super.visitFunctionExpression(tree);
  }

  private void checkFunction(FunctionTree tree) {
    ControlFlowGraph cfg = ControlFlowGraph.build(tree, context());
    if (cfg == null) {
      return;
    }
    // Only consider reachable blocks to avoid confusing issues (+Unreachable blocks are reported by S1763)
    Set cfgBlocks = getReachableBlocks(cfg);

    Map blockSummaries = new HashMap<>();
    cfgBlocks.forEach(b -> blockSummaries.put(b, getBlockSummary(b)));

    Set providedVariables = getParameterVariableNames(tree);
    if (tree.is(Kind.FUNCTION_EXPRESSION)) {
      providedVariables.addAll(getLexicalVariableNames((FunctionExpressionTree) tree));
    }

    InitialDataCollector initialDataCollector = new InitialDataCollector();
    tree.accept(initialDataCollector);
    // Catch clauses do not appear in the CFG. We collect all exception variables in the function body and
    // consider them as provided in the whole function body to avoid false positives.
    providedVariables.addAll(initialDataCollector.exceptionVariables);

    blockSummaries.get(cfg.start()).stateOnBlockStart.initializedVariables.addAll(providedVariables);

    Deque workList = new ArrayDeque<>(cfgBlocks);
    while (!workList.isEmpty()) {
      CfgBlock block = workList.pop();
      BlockSummary summary = blockSummaries.get(block);

      for (CfgBlock successor : block.successors()) {
        StateOnBlockStart stateOnBlockStart = blockSummaries.get(successor).stateOnBlockStart;
        if (!stateOnBlockStart.containsBlockSummary(summary)) {
          stateOnBlockStart.addBlockSummary(summary);
          workList.add(successor);
        }
      }
    }

    Map> uninitializedVariableUses = new HashMap<>();
    cfgBlocks.forEach(b -> checkBlock(b, blockSummaries.get(b))
      .forEach((v, s) -> uninitializedVariableUses.computeIfAbsent(v, st -> new HashSet<>()).addAll(s)));

    // static variables could be initialized after first usage. We only raise an issue on them when they were never
    // initialized (i.e., not initialized by predecessors of the end block)
    uninitializedVariableUses.entrySet().stream()
      .filter(e -> !isInitializedStaticVariable(e.getKey(),
        initialDataCollector.uninitializedStaticVariables,
        blockSummaries.get(cfg.end())))
      .forEach(e -> reportOnFirstTree(e.getValue()));
  }

  private static boolean isInitializedStaticVariable(String var, Set uninitializedStaticVariables, BlockSummary endBlockSummary) {
    return uninitializedStaticVariables.contains(var) && endBlockSummary.stateOnBlockStart.wasInitialized(var);
  }

  private void reportOnFirstTree(Set trees) {
    trees.stream()
      .min(Comparator.comparingInt(a -> ((PHPTree) a).getFirstToken().line())
        .thenComparing(a -> ((PHPTree) a).getFirstToken().column()))
      .ifPresent(t -> newIssue(t, MESSAGE));
  }

  private static Set getReachableBlocks(ControlFlowGraph cfg) {
    Set result = new HashSet<>();
    result.add(cfg.start());

    Deque workList = new ArrayDeque<>(result);
    while (!workList.isEmpty()) {
      CfgBlock item = workList.pop();

      Set newSuccessors = item.successors().stream()
        .filter(b -> !result.contains(b))
        .collect(Collectors.toSet());

      result.addAll(newSuccessors);
      workList.addAll(newSuccessors);
    }

    return result;
  }

  private static Map> checkBlock(CfgBlock block, BlockSummary blockSummary) {
    Map> result = new HashMap<>();

    UninitializedUsageFindVisitor visitor = new UninitializedUsageFindVisitor(blockSummary.stateOnBlockStart);
    for (Tree element : block.elements()) {
      element.accept(visitor);
    }
    visitor.uninitializedVariableReads.entrySet().stream()
      .filter(e -> !PREDEFINED_VARIABLES.contains(e.getKey().toUpperCase(Locale.ROOT)))
      .forEach(e -> result.computeIfAbsent(e.getKey(), v -> new HashSet<>()).add(e.getValue()));

    return result;
  }

  private static Set getLexicalVariableNames(FunctionExpressionTree tree) {
    Set result = new HashSet<>();

    if (tree.lexicalVars() != null) {
      tree.lexicalVars().variables().stream()
        .map(VariableTree::variableExpression)
        .filter(v -> v.is(Kind.VARIABLE_IDENTIFIER))
        .forEach(v -> result.add(((VariableIdentifierTree) v).variableExpression().text()));
    }

    return result;
  }

  private static Set getParameterVariableNames(FunctionTree tree) {
    return tree.parameters().parameters().stream()
      .map(p -> p.variableIdentifier().variableExpression().text())
      .collect(Collectors.toSet());
  }

  private static Set getForEachVariables(ForEachStatementTree tree) {
    Set result = new HashSet<>();

    if (tree.value().is(Kind.VARIABLE_IDENTIFIER)) {
      result.add(((VariableIdentifierTree) tree.value()).variableExpression().text());
    } else {
      TreeUtils.descendants(tree.value(), VariableIdentifierTree.class)
        .forEach(v -> result.add(v.variableExpression().text()));
    }

    if (tree.key() != null) {
      if (tree.key().is(Kind.VARIABLE_IDENTIFIER)) {
        result.add(((VariableIdentifierTree) tree.key()).variableExpression().text());
      } else {
        TreeUtils.descendants(tree.key(), VariableIdentifierTree.class).forEach(v -> result.add(v.variableExpression().text()));
      }
    }

    return result;
  }

  private static BlockSummary getBlockSummary(CfgBlock block) {
    SummaryCreationVisitor visitor = new SummaryCreationVisitor();
    for (Tree element : block.elements()) {
      element.accept(visitor);
    }
    BlockSummary summary = new BlockSummary(visitor.initializedVariables, visitor.scopeWasChanged);

    if (block instanceof CfgBranchingBlock branchingBlock) {
      var branchingTree = branchingBlock.branchingTree();
      if (branchingTree.is(Kind.FOREACH_STATEMENT, Kind.ALTERNATIVE_FOREACH_STATEMENT)) {
        summary.initializedVariables.addAll(getForEachVariables((ForEachStatementTree) branchingTree));
      }
    }

    return summary;
  }

  private static class BlockSummary {
    protected final StateOnBlockStart stateOnBlockStart = new StateOnBlockStart();
    protected Set initializedVariables;
    protected boolean scopeWasChangedLocally;

    public BlockSummary(Set initializedVariables, boolean scopeWasChangedLocally) {
      this.initializedVariables = new HashSet<>(initializedVariables);
      this.scopeWasChangedLocally = scopeWasChangedLocally;
    }

    protected Set allVariables() {
      HashSet result = new HashSet<>(initializedVariables);
      result.addAll(stateOnBlockStart.initializedVariables);
      return result;
    }

    private boolean scopeWasChanged() {
      return scopeWasChangedLocally || stateOnBlockStart.scopeWasChanged;
    }
  }

  private static class StateOnBlockStart {
    protected final Set initializedVariables = new HashSet<>();
    protected boolean scopeWasChanged = false;

    protected boolean wasInitialized(String variable) {
      return initializedVariables.contains(variable);
    }

    private boolean containsBlockSummary(BlockSummary blockSummary) {
      return initializedVariables.containsAll(blockSummary.allVariables())
        && scopeWasChanged == blockSummary.scopeWasChanged();
    }

    private void addBlockSummary(BlockSummary blockSummary) {
      initializedVariables.addAll(blockSummary.allVariables());
      scopeWasChanged = scopeWasChanged || blockSummary.scopeWasChanged();
    }
  }

  private static boolean isReadAccess(Tree tree) {
    Predicate predicate = IS_READ_ACCESS_BY_PARENT_KIND.get(tree.getParent().getKind());
    return predicate == null || predicate.test(tree);
  }

  private static final Map> IS_READ_ACCESS_BY_PARENT_KIND = initializeReadPredicate();

  private static Map> initializeReadPredicate() {
    Map> map = new EnumMap<>(Kind.class);

    PARENT_INITIALIZATION_KIND.forEach(kind -> map.put(kind, tree -> false));
    map.put(Kind.ASSIGNMENT, tree -> tree == ((AssignmentExpressionTree) tree.getParent()).value());
    map.put(Kind.CALL_ARGUMENT, tree -> {
      if (!tree.getParent().getParent().is(Kind.FUNCTION_CALL)) {
        return false;
      }
      FunctionCallTree functionCall = (FunctionCallTree) tree.getParent().getParent();
      return tree == functionCall.callee() || FUNCTION_ALLOWING_ARGUMENT_CHECK.contains(CheckUtils.getLowerCaseFunctionName(functionCall));
    });
    map.put(Kind.ARRAY_ACCESS, tree -> !isArrayAssignment(tree));
    map.put(Kind.PARENTHESISED_EXPRESSION, tree -> isReadAccess(tree.getParent()));

    return map;
  }

  private static boolean isArrayAssignment(Tree tree) {
    Tree child = skipParentArrayAccess(tree);
    return child.getParent().is(Kind.ASSIGNMENT) &&
      ((AssignmentExpressionTree) child.getParent()).variable() == child;
  }

  private static Tree skipParentArrayAccess(Tree tree) {
    Tree child = tree;
    while (child.getParent().is(Kind.ARRAY_ACCESS) && ((ArrayAccessTree) child.getParent()).object() == child) {
      child = child.getParent();
    }
    return child;
  }

  private static boolean uninitializedVariableDeclaration(VariableIdentifierTree tree) {
    return (tree.getParent().is(Kind.VARIABLE_DECLARATION) &&
      ((VariableDeclarationTree) tree.getParent()).equalToken() == null);
  }

  private static class UninitializedUsageFindVisitor extends ScopeVisitor {
    private final StateOnBlockStart stateOnBlockStart;
    private final Map uninitializedVariableReads = new HashMap<>();

    private UninitializedUsageFindVisitor(StateOnBlockStart stateOnBlockStart) {
      this.stateOnBlockStart = stateOnBlockStart;
    }

    @Override
    public void visitVariableIdentifier(VariableIdentifierTree tree) {
      if (isClassMemberAccess(tree) || uninitializedVariableDeclaration(tree)) {
        return;
      }

      String name = tree.variableExpression().text();
      if (!isReadAccess(tree)) {
        initializedVariables.add(name);
      } else if (!isInitializedRead(name)) {
        uninitializedVariableReads.putIfAbsent(name, tree);
      }

      super.visitVariableIdentifier(tree);
    }

    private boolean isInitializedRead(String variable) {
      if (scopeWasChanged || stateOnBlockStart.scopeWasChanged) {
        return true;
      }

      return initializedVariables.contains(variable) || stateOnBlockStart.initializedVariables.contains(variable);
    }
  }

  private static class SummaryCreationVisitor extends ScopeVisitor {
    @Override
    public void visitVariableIdentifier(VariableIdentifierTree tree) {
      if (isClassMemberAccess(tree) || uninitializedVariableDeclaration(tree)) {
        return;
      }

      if (!isReadAccess(tree)) {
        initializedVariables.add(tree.variableExpression().text());
      }

      super.visitVariableIdentifier(tree);
    }
  }

  private abstract static class ScopeVisitor extends PHPVisitorCheck {
    protected final Set initializedVariables = new HashSet<>();
    protected boolean scopeWasChanged = false;

    @Override
    public void visitFunctionDeclaration(FunctionDeclarationTree tree) {
      // do not visit nested functions
    }

    @Override
    public void visitFunctionExpression(FunctionExpressionTree tree) {
      if (tree.lexicalVars() != null) {
        tree.lexicalVars().accept(this);
      }
      // do not visit nested body
    }

    @Override
    public void visitFunctionCall(FunctionCallTree functionCall) {
      String lowerCaseFunctionName = CheckUtils.getLowerCaseFunctionName(functionCall);
      if (lowerCaseFunctionName != null && FUNCTION_CHANGING_CURRENT_SCOPE.contains(lowerCaseFunctionName)) {
        scopeWasChanged = true;
      }
      super.visitFunctionCall(functionCall);
    }

    protected static boolean isClassMemberAccess(Tree tree) {
      Tree child = skipParentArrayAccess(tree);
      return (child.getParent().is(Kind.CLASS_MEMBER_ACCESS) &&
        ((MemberAccessTree) child.getParent()).member() == child);
    }
  }

  private static class InitialDataCollector extends PHPVisitorCheck {
    private final Set exceptionVariables = new HashSet<>();
    private final Set uninitializedStaticVariables = new HashSet<>();

    @Override
    public void visitCatchBlock(CatchBlockTree tree) {
      if (tree.variable() != null) {
        exceptionVariables.add(tree.variable().variableExpression().text());
      }
      super.visitCatchBlock(tree);
    }

    @Override
    public void visitVariableIdentifier(VariableIdentifierTree tree) {
      if (uninitializedVariableDeclaration(tree)
        && TreeUtils.findAncestorWithKind(tree, Kind.STATIC_STATEMENT) != null) {
        uninitializedStaticVariables.add(tree.variableExpression().text());
      }
      super.visitVariableIdentifier(tree);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy