org.sonar.python.checks.cdk.CdkUtils Maven / Gradle / Ivy
/*
* SonarQube Python Plugin
* Copyright (C) 2011-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.python.checks.cdk;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.sonar.plugins.python.api.IssueLocation;
import org.sonar.plugins.python.api.PythonCheck;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.tree.Argument;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
import org.sonar.plugins.python.api.tree.DictionaryLiteralElement;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.KeyValuePair;
import org.sonar.plugins.python.api.tree.ListLiteral;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.NumericLiteral;
import org.sonar.plugins.python.api.tree.RegularArgument;
import org.sonar.plugins.python.api.tree.StringLiteral;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.UnpackingExpression;
import org.sonar.python.checks.utils.Expressions;
import org.sonar.python.tree.TreeUtils;
import static org.sonar.python.checks.cdk.CdkPredicate.isListLiteral;
import static org.sonar.python.checks.cdk.CdkPredicate.isString;
public class CdkUtils {
private CdkUtils() {
}
public static Optional getInt(Expression expression) {
try {
return Optional.of((int)((NumericLiteral) expression).valueAsLong());
} catch (ClassCastException e) {
return Optional.empty();
}
}
public static Optional getString(Expression expression) {
try {
return Optional.of(((StringLiteral) expression).trimmedQuotesValue());
} catch (ClassCastException e) {
return Optional.empty();
}
}
public static Optional getCall(Expression expression, String fqn) {
if (expression.is(Tree.Kind.CALL_EXPR) && CdkPredicate.isFqn(fqn).test(expression)) {
return Optional.of((CallExpression) expression);
}
return Optional.empty();
}
public static Optional getListExpression(ExpressionFlow expression) {
return expression.getExpression(isListLiteral()).map(ListLiteral.class::cast);
}
public static Optional getDictionary(Expression expression) {
if (expression.is(Tree.Kind.DICTIONARY_LITERAL)) {
return Optional.of((DictionaryLiteral) expression);
}
return Optional.empty();
}
/**
* Resolve a particular argument of a call by keyword or get an empty optional if the argument is not set nor resolvable.
*/
protected static Optional getArgument(SubscriptionContext ctx, CallExpression callExpression, String argumentName) {
List arguments = callExpression.arguments();
return resolveNamedArgument(ctx, arguments, argumentName)
.or(() -> resolveUnpackingExpression(ctx, arguments, argumentName));
}
private static Optional resolveNamedArgument(SubscriptionContext ctx, List arguments, String argumentName) {
RegularArgument regArgument = TreeUtils.argumentByKeyword(argumentName, arguments);
return Optional.ofNullable(regArgument)
.map(RegularArgument::expression)
.map(expression -> ExpressionFlow.build(ctx, expression));
}
private static Optional resolveUnpackingExpression(SubscriptionContext ctx, List arguments, String argumentName) {
return arguments.stream()
.filter(UnpackingExpression.class::isInstance)
.map(UnpackingExpression.class::cast)
.map(UnpackingExpression::expression)
.map(expression -> ExpressionFlow.build(ctx, expression, argumentName))
.findFirst();
}
/**
* Resolve a particular argument of a call by keyword or position. Return an empty optional if the argument is not set nor resolvable.
*/
public static Optional getArgument(SubscriptionContext ctx, CallExpression callExpression, String argumentName, int argumentPosition) {
return Optional.ofNullable(TreeUtils.nthArgumentOrKeyword(argumentPosition, argumentName, callExpression.arguments()))
.map(argument -> ExpressionFlow.build(ctx, argument.expression()));
}
/**
* Returns a ListLiteral if the given expression flow origins of this kind
*/
public static Optional getList(ExpressionFlow flow) {
return flow.getExpression(e -> e.is(Tree.Kind.LIST_LITERAL))
.map(ListLiteral.class::cast);
}
/**
* Creates flows for the individual elements of a list
*/
public static List getListElements(SubscriptionContext ctx, ListLiteral list) {
return list.elements().expressions().stream()
.map(expression -> CdkUtils.ExpressionFlow.build(ctx, expression))
.toList();
}
/**
* Returns a DictionaryLiteral if the given expression flow origins of this kind
*/
public static Optional getDictionary(ExpressionFlow flow) {
return flow.getExpression(e -> e.is(Tree.Kind.DICTIONARY_LITERAL))
.map(DictionaryLiteral.class::cast);
}
/**
* By resolving the individual dictionary elements, a key-value pair can be returned by a given key. The value is also a resolved flow.
*/
public static Optional getDictionaryPair(SubscriptionContext ctx, DictionaryLiteral dict, String key) {
return getDictionaryPair(CdkUtils.resolveDictionary(ctx, dict), key);
}
/**
* A key-value pair can be returned by a given key. The value is also a resolved flow.
*/
public static Optional getDictionaryPair(List pairs, String key) {
return pairs.stream()
.filter(pair -> pair.key.hasExpression(isString(key)))
.findFirst();
}
/**
* Return a resolved dictionary value by a given key
*/
public static Optional getDictionaryValue(List pairs, String key) {
return getDictionaryPair(pairs, key)
.map(pair -> pair.value);
}
/**
* Collects all dictionary elements of a list as a return.
*/
public static List getDictionaryInList(SubscriptionContext ctx, ListLiteral listeners) {
return getListElements(ctx, listeners).stream()
.map(CdkUtils::getDictionary)
.flatMap(Optional::stream)
.toList();
}
/**
* Resolves all elements of a dictionary. All keys and values are resolved into flows.
*/
public static List resolveDictionary(SubscriptionContext ctx, DictionaryLiteral dict) {
return dict.elements().stream()
.map(e -> CdkUtils.getKeyValuePair(ctx, e))
.flatMap(Optional::stream)
.toList();
}
/**
* Resolve the key and value of a dictionary element or get an empty optional if the element is an UnpackingExpression.
*/
public static Optional getKeyValuePair(SubscriptionContext ctx, DictionaryLiteralElement element) {
return element.is(Tree.Kind.KEY_VALUE_PAIR) ? Optional.of(ResolvedKeyValuePair.build(ctx, (KeyValuePair) element)) : Optional.empty();
}
/**
* The expression flow represents the propagation of an expression.
* It serves as a resolution path from the use of the expression up to the value assignment.
* For example, if the value of an argument expression did not occur directly in the function call, the value can be tracked back.
* The flow allows on the one hand to check the assigned value
* and on the other hand to display the assignment path of the relevant value when creating an issue.
*/
static class ExpressionFlow {
private static final String TAIL_MESSAGE = "Propagated setting.";
private final SubscriptionContext ctx;
private final Deque locations;
private ExpressionFlow(SubscriptionContext ctx, Deque locations) {
this.ctx = ctx;
this.locations = locations;
}
protected static ExpressionFlow build(SubscriptionContext ctx, Expression expression) {
return build(ctx, expression, null);
}
protected static ExpressionFlow build(SubscriptionContext ctx, Expression expression, @Nullable String argumentName) {
Set locations = new LinkedHashSet<>();
resolveLocations(ctx, expression, locations, argumentName);
return new ExpressionFlow(ctx, new LinkedList<>(locations));
}
static void resolveLocations(SubscriptionContext ctx, Expression expression, Set locations, @Nullable String argumentName) {
// Handle dicts as part of unpacking expressions
if (argumentName != null && expression.is(Tree.Kind.DICTIONARY_LITERAL)) {
getDictionaryPair(ctx, (DictionaryLiteral) expression, argumentName).ifPresent(
argumentPair -> locations.addAll(argumentPair.value.locations())
);
return;
}
// Do not add an entire dict to locations, only the relevant key value pair
locations.add(expression);
if (expression.is(Tree.Kind.NAME)) {
Expression singleAssignedValue = Expressions.singleAssignedValue(((Name) expression));
if (singleAssignedValue != null && !locations.contains(singleAssignedValue)) {
resolveLocations(ctx, singleAssignedValue, locations, argumentName);
}
}
}
public void addIssue(String primaryMessage, IssueLocation... secondaryLocations) {
PythonCheck.PreciseIssue issue = ctx.addIssue(locations.getFirst().parent(), primaryMessage);
locations.stream().skip(1).forEach(expression -> issue.secondary(expression.parent(), TAIL_MESSAGE));
Stream.of(secondaryLocations).forEach(issue::secondary);
}
public void addIssueIf(Predicate predicate, String primaryMessage, IssueLocation... secondaryLocations) {
if (hasExpression(predicate)) {
addIssue(primaryMessage, secondaryLocations);
}
}
public void addIssueIf(Predicate predicate, String primaryMessage, CallExpression call) {
if (hasExpression(predicate)) {
ctx.addIssue(call.callee(), primaryMessage);
}
}
public boolean hasExpression(Predicate predicate) {
return locations.stream().anyMatch(predicate);
}
public Optional getExpression(Predicate predicate) {
return locations.stream().filter(predicate).findFirst();
}
public Deque locations() {
return locations;
}
public Expression getLast() {
return locations().getLast();
}
public IssueLocation asSecondaryLocation(String message) {
return IssueLocation.preciseLocation(getLast().parent(), message);
}
public SubscriptionContext ctx() {
return ctx;
}
}
/**
* Dataclass to store a resolved KeyValuePair structure
*/
static class ResolvedKeyValuePair {
final ExpressionFlow key;
final ExpressionFlow value;
private ResolvedKeyValuePair(ExpressionFlow key, ExpressionFlow value) {
this.key = key;
this.value = value;
}
static ResolvedKeyValuePair build(SubscriptionContext ctx, KeyValuePair pair) {
return new ResolvedKeyValuePair(ExpressionFlow.build(ctx, pair.key()), ExpressionFlow.build(ctx, pair.value()));
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy