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

org.sonar.iac.docker.symbols.ArgumentResolution Maven / Gradle / Ivy

/*
 * SonarQube IaC Plugin
 * Copyright (C) 2021-2023 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.iac.docker.symbols;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.iac.docker.tree.api.Argument;
import org.sonar.iac.docker.tree.api.DockerTree;
import org.sonar.iac.docker.tree.api.EncapsulatedVariable;
import org.sonar.iac.docker.tree.api.ExpandableStringCharacters;
import org.sonar.iac.docker.tree.api.ExpandableStringLiteral;
import org.sonar.iac.docker.tree.api.Expression;
import org.sonar.iac.docker.tree.api.KeyValuePair;
import org.sonar.iac.docker.tree.api.Literal;
import org.sonar.iac.docker.tree.api.SyntaxToken;
import org.sonar.iac.docker.tree.api.Variable;
import org.sonarsource.analyzer.commons.collections.ListUtils;

/**
 * Class dedicated to resolving {@link Argument}, which is a complex object used to represent most parameters of instruction.
 */
public class ArgumentResolution {

  public enum Status {
    RESOLVED,
    UNRESOLVED,
    EMPTY
  }

  static final ArgumentResolution EMPTY = new ArgumentResolution(null, "", Status.EMPTY) {
    @Override
    public Argument argument() {
      throw new IllegalStateException("The root argument should not be requested from an empty resolution");
    }
  };

  private final String value;
  private final Status status;

  @Nullable
  private final Argument argument;

  public ArgumentResolution(@Nullable Argument argument, String value, Status status) {
    this.argument = argument;
    this.value = value;
    this.status = status;
  }

  /**
   * Main method of the class, the point of entry to resolve {@link Argument} in order to exploit the result in checks.
   * In docker, most instruction can expect one or even multiple arguments, which can be a mix of strings, quotes string with spaces,
   * variable reference with or without modifier, ect.
   * This method will provide an ArgumentResolution object with the result of the tentative of resolution: the status and the String value.
   * The quotes and double quotes in string literals are striped by default.
   */
  public static ArgumentResolution of(@Nullable Argument argument) {
    return ArgumentResolver.resolve(argument, true);
  }

  /**
   * The method is similar to {@code ArgumentResolution#of} but there is a control of strip quotes or double quotes in string literal.
   */
  public static ArgumentResolution ofWithoutStrippingQuotes(@Nullable Argument argument) {
    return ArgumentResolver.resolve(argument, false);
  }

  public String value() {
    return value;
  }

  public Status status() {
    return status;
  }

  public Argument argument() {
    return argument;
  }

  public boolean isResolved() {
    return this.status == Status.RESOLVED;
  }

  public boolean isUnresolved() {
    return this.status == Status.UNRESOLVED;
  }

  public boolean isEmpty() {
    return this.status == Status.EMPTY;
  }

  private static class Builder {

    private final Argument argument;
    private Status status = Status.RESOLVED;
    private final StringBuilder sb = new StringBuilder();

    public Builder(Argument argument) {
      this.argument = argument;
    }

    private void addValue(String value) {
      sb.append(value);
    }

    private void setUnresolved() {
      status = Status.UNRESOLVED;
    }

    public ArgumentResolution build() {
      return new ArgumentResolution(argument, sb.toString(), status);
    }
  }

  private static class ArgumentResolver {

    private final boolean stripQuotes;
    Builder builder;
    Set visitedVariable = new HashSet<>();

    private ArgumentResolver(boolean stripQuotes) {
      this.stripQuotes = stripQuotes;
    }

    private static ArgumentResolution resolve(@Nullable Argument argument, boolean stripQuotes) {
      return new ArgumentResolver(stripQuotes).resolveArgument(argument);
    }

    private ArgumentResolution resolveArgument(@Nullable Argument argument) {
      if (argument == null) {
        return ArgumentResolution.EMPTY;
      }
      builder = new Builder(argument);
      resolveExpressions(argument.expressions());
      return builder.build();
    }

    private void resolveExpressions(List expressions) {
      for (Expression expression : expressions) {
        resolveExpression(expression);
      }
    }

    private void resolveExpression(Expression expression) {
      switch (expression.getKind()) {
        case STRING_LITERAL:
          if (shouldKeepQuotes(expression)) {
            builder.addValue(((Literal) expression).originalValue());
          } else {
            builder.addValue(((Literal) expression).value());
          }
          break;
        case EXPANDABLE_STRING_CHARACTERS:
          builder.addValue(((ExpandableStringCharacters) expression).value());
          break;
        case EXPANDABLE_STRING_LITERAL:
          maybeAddQuote(expression, ((ExpandableStringLiteral) expression).getOpenDoubleQuote());
          resolveExpressions(((ExpandableStringLiteral) expression).expressions());
          maybeAddQuote(expression, ((ExpandableStringLiteral) expression).getCloseDoubleQuote());
          break;
        case REGULAR_VARIABLE:
          resolveVariable((Variable) expression);
          break;
        case ENCAPSULATED_VARIABLE:
          EncapsulatedVariable encapsulatedVariable = (EncapsulatedVariable) expression;
          if (!":+".equals((encapsulatedVariable).modifierSeparator())) {
            resolveVariable(encapsulatedVariable);
          } else {
            builder.setUnresolved();
          }
          break;
        default:
          builder.setUnresolved();
      }
    }

    private boolean shouldKeepQuotes(Expression expression) {
      return !stripQuotes && expression.parent() != null && expression.parent().parent().is(DockerTree.Kind.SHELL_FORM);
    }

    private void maybeAddQuote(Expression expression, SyntaxToken quote) {
      if (shouldKeepQuotes(expression)) {
        builder.addValue(quote.value());
      }
    }

    /**
     * To resolve the value of a symbol at a given state, the last assigned value is considered.
     * There for all symbol usages are analyzed for the last assignment with value before the access.
     */
    private void resolveVariable(Variable variable) {
      Symbol symbol = variable.symbol();
      if (!visitedVariable.add(variable) || symbol == null) {
        builder.setUnresolved();
        return;
      }

      List usages = ListUtils.reverse(symbol.usages());
      List reversedAssignments = usages.stream()
        .filter(usage -> usage.kind().equals(Usage.Kind.ASSIGNMENT))
        .collect(Collectors.toList());
      Scope.Kind accessScopeKind = usages.get(0).scope().kind();

      Argument lastAssignedValue = findLastAccessibleAssignedValue(reversedAssignments, accessScopeKind);
      if (lastAssignedValue != null) {
        resolveExpressions(lastAssignedValue.expressions());
      } else {
        builder.setUnresolved();
      }
    }

    /**
     * In Dockerfiles exit two kinds of scopes where variables can be defined and accessed.
     * To access a variable from the global scope inside a DockerImage scope the variable access has to be enabled
     * by an assignment instruction of the variable without value.
     */
    @Nullable
    private static Argument findLastAccessibleAssignedValue(List assignments, Scope.Kind accessScopeKind) {
      boolean hasAccessToGlobalScope = false;
      for (Usage assignment : assignments) {
        if (assignment.tree().is(DockerTree.Kind.KEY_VALUE_PAIR)) {
          KeyValuePair assignmentTree = (KeyValuePair) assignment.tree();
          Argument value = assignmentTree.value();
          if (value != null) {
            return (assignment.scope().kind().equals(accessScopeKind) || hasAccessToGlobalScope) ? value : null;
          } else {
            hasAccessToGlobalScope = true;
          }
        }
      }
      return null;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy