Skip to content

Commit 72de2ba

Browse files
SONARPY-1147 Refactoring before release of SonarPython 3.18
1 parent a4de8f5 commit 72de2ba

18 files changed

+408
-321
lines changed

python-checks/src/main/java/org/sonar/python/checks/cdk/AbstractCdkResourceCheck.java

Lines changed: 14 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,24 @@
1919
*/
2020
package org.sonar.python.checks.cdk;
2121

22-
import java.util.ArrayList;
23-
import java.util.Collections;
22+
import java.util.Collection;
2423
import java.util.HashMap;
25-
import java.util.List;
2624
import java.util.Map;
2725
import java.util.Optional;
2826
import java.util.function.BiConsumer;
29-
import java.util.function.Predicate;
3027
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
3128
import org.sonar.plugins.python.api.SubscriptionCheck;
3229
import org.sonar.plugins.python.api.SubscriptionContext;
3330
import org.sonar.plugins.python.api.symbols.Symbol;
3431
import org.sonar.plugins.python.api.tree.CallExpression;
35-
import org.sonar.plugins.python.api.tree.Expression;
36-
import org.sonar.plugins.python.api.tree.Name;
37-
import org.sonar.plugins.python.api.tree.RegularArgument;
38-
import org.sonar.plugins.python.api.tree.StringLiteral;
39-
import org.sonar.plugins.python.api.tree.Token;
4032
import org.sonar.plugins.python.api.tree.Tree;
41-
import org.sonar.python.checks.Expressions;
42-
import org.sonar.python.tree.TreeUtils;
4333

34+
/**
35+
* Since most CDK related checks check arguments of method calls or object initializations,
36+
* this abstract class can be used to register CallExpression consumers for various fully qualified names.
37+
* For this purpose the method {@link #checkFqn(String, BiConsumer)} or {@link #checkFqns(Collection, BiConsumer)}
38+
* must be called in the {@link #registerFqnConsumer()} method which has to be implemented.
39+
*/
4440
public abstract class AbstractCdkResourceCheck extends PythonSubscriptionCheck {
4541

4642
private final Map<String, BiConsumer<SubscriptionContext, CallExpression>> fqnCallConsumers = new HashMap<>();
@@ -59,150 +55,22 @@ protected void visitNode(SubscriptionContext ctx) {
5955
.ifPresent(consumer -> consumer.accept(ctx, node));
6056
}
6157

62-
// TODO should be abstract when resourceFqn and visitResourceConstructor are dropped
63-
protected void registerFqnConsumer() {
64-
checkFqn(resourceFqn(), this::visitResourceConstructor);
65-
}
66-
67-
protected void checkFqn(String fqn, BiConsumer<SubscriptionContext, CallExpression> consumer) {
68-
fqnCallConsumers.put(fqn, consumer);
69-
}
70-
71-
protected String resourceFqn() {
72-
throw new UnsupportedOperationException("Override at least resourceFqn or registerFqnConsumer");
73-
}
74-
75-
protected void visitResourceConstructor(SubscriptionContext ctx, CallExpression resourceConstructor) {
76-
throw new UnsupportedOperationException("When using resourceFqn override this method");
77-
}
78-
79-
protected static Optional<ArgumentTrace> getArgument(SubscriptionContext ctx, CallExpression callExpression, String argumentName) {
80-
return callExpression.arguments().stream()
81-
.map(RegularArgument.class::cast)
82-
.filter(regularArgument -> regularArgument.keywordArgument() != null)
83-
.filter(regularArgument -> argumentName.equals(regularArgument.keywordArgument().name()))
84-
.map(regularArgument -> ArgumentTrace.build(ctx, regularArgument.expression()))
85-
.findAny();
86-
}
58+
protected abstract void registerFqnConsumer();
8759

8860
/**
89-
* For compatibility with other classes and branches.
90-
* TODO Can be removed at the end of the sprint to reduce complexity.
61+
* Register a consumer for a single FQN
9162
*/
92-
public static class ArgumentTrace extends ExpressionTrace {
93-
private ArgumentTrace(SubscriptionContext ctx, List<Expression> trace) {
94-
super(ctx, trace);
95-
}
96-
97-
protected static ArgumentTrace build(SubscriptionContext ctx, Expression expression) {
98-
List<Expression> trace = new ArrayList<>();
99-
buildTrace(expression, trace);
100-
return new ArgumentTrace(ctx, trace);
101-
}
102-
}
103-
104-
static class ExpressionTrace {
105-
106-
private static final String TAIL_MESSAGE = "Propagated setting.";
107-
108-
private final SubscriptionContext ctx;
109-
private final List<Expression> trace;
110-
111-
private ExpressionTrace(SubscriptionContext ctx, List<Expression> trace) {
112-
this.ctx = ctx;
113-
this.trace = Collections.unmodifiableList(trace);
114-
}
115-
protected static ExpressionTrace build(SubscriptionContext ctx, Expression expression) {
116-
List<Expression> trace = new ArrayList<>();
117-
buildTrace(expression, trace);
118-
return new ExpressionTrace(ctx, trace);
119-
}
120-
121-
static void buildTrace(Expression expression, List<Expression> trace) {
122-
trace.add(expression);
123-
if (expression.is(Tree.Kind.NAME)) {
124-
Expression singleAssignedValue = Expressions.singleAssignedValue(((Name) expression));
125-
if (singleAssignedValue != null && !trace.contains(singleAssignedValue)) {
126-
buildTrace(singleAssignedValue, trace);
127-
}
128-
}
129-
}
130-
131-
public void addIssue(String primaryMessage) {
132-
PreciseIssue issue = ctx.addIssue(trace.get(0).parent(), primaryMessage);
133-
trace.stream().skip(1).forEach(expression -> issue.secondary(expression.parent(), TAIL_MESSAGE));
134-
}
135-
136-
public void addIssueIf(Predicate<Expression> predicate, String primaryMessage) {
137-
if (hasExpression(predicate)) {
138-
addIssue(primaryMessage);
139-
}
140-
}
141-
142-
public void addIssueIf(Predicate<Expression> predicate, String primaryMessage, CallExpression call) {
143-
if (hasExpression(predicate)) {
144-
ctx.addIssue(call.callee(), primaryMessage);
145-
}
146-
}
147-
148-
public boolean hasExpression(Predicate<Expression> predicate) {
149-
return trace.stream().anyMatch(predicate);
150-
}
151-
152-
public Optional<Expression> getExpression(Predicate<Expression> predicate) {
153-
return trace.stream().filter(predicate).findFirst();
154-
}
155-
156-
public List<Expression> trace() {
157-
return trace;
158-
}
159-
}
160-
161-
protected static Predicate<Expression> isFalse() {
162-
return expression -> Optional.ofNullable(expression.firstToken()).map(Token::value).filter("False"::equals).isPresent();
163-
}
164-
165-
protected static Predicate<Expression> isNone() {
166-
return expression -> expression.is(Tree.Kind.NONE);
167-
}
168-
169-
protected static Predicate<Expression> isFqn(String fqnValue) {
170-
return expression -> Optional.ofNullable(TreeUtils.fullyQualifiedNameFromExpression(expression))
171-
.filter(fqnValue::equals)
172-
.isPresent();
173-
}
174-
175-
protected static Optional<String> getStringValue(Expression expression) {
176-
try {
177-
return Optional.of(((StringLiteral) expression).trimmedQuotesValue());
178-
} catch (ClassCastException e) {
179-
return Optional.empty();
180-
}
63+
protected void checkFqn(String fqn, BiConsumer<SubscriptionContext, CallExpression> consumer) {
64+
fqnCallConsumers.put(fqn, consumer);
18165
}
18266

18367
/**
184-
* @return Predicate which tests if expression is a string and is equal the expected value
68+
* Register a consumer for multiple FQNs
18569
*/
186-
protected static Predicate<Expression> isStringValue(String expectedValue) {
187-
return expression -> getStringValue(expression).filter(expectedValue::equals).isPresent();
70+
protected void checkFqns(Collection<String> suffixes, BiConsumer<SubscriptionContext, CallExpression> consumer) {
71+
suffixes.forEach(suffix -> checkFqn(suffix, consumer));
18872
}
18973

190-
protected static Predicate<Expression> isSensitiveMethod(SubscriptionContext ctx, String methodFqn, String argName, Predicate<Expression> sensitiveValuePredicate) {
191-
return expression -> {
192-
if (!isFqn(methodFqn).test(expression)) {
193-
return false;
194-
}
195-
if (!expression.is(Tree.Kind.CALL_EXPR)) {
196-
return true;
197-
}
19874

199-
Optional<AbstractCdkResourceCheck.ArgumentTrace> argTrace = getArgument(ctx, (CallExpression) expression, argName);
200-
if (argTrace.isEmpty()) {
201-
return true;
202-
}
203-
204-
return argTrace.filter(trace -> trace.hasExpression(sensitiveValuePredicate)).isPresent();
205-
};
206-
}
20775

20876
}

python-checks/src/main/java/org/sonar/python/checks/cdk/AbstractS3BucketCheck.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*/
2020
package org.sonar.python.checks.cdk;
2121

22+
import java.util.function.BiConsumer;
2223
import org.sonar.plugins.python.api.SubscriptionContext;
2324
import org.sonar.plugins.python.api.tree.CallExpression;
2425

@@ -27,14 +28,9 @@ public abstract class AbstractS3BucketCheck extends AbstractCdkResourceCheck {
2728
protected static final String S3_BUCKET_FQN = "aws_cdk.aws_s3.Bucket";
2829

2930
@Override
30-
protected String resourceFqn() {
31-
return S3_BUCKET_FQN;
31+
protected void registerFqnConsumer() {
32+
checkFqn(S3_BUCKET_FQN, this.visitBucketConstructor());
3233
}
3334

34-
@Override
35-
protected void visitResourceConstructor(SubscriptionContext ctx, CallExpression resourceConstructor) {
36-
visitBucketConstructor(ctx, resourceConstructor);
37-
}
38-
39-
abstract void visitBucketConstructor(SubscriptionContext ctx, CallExpression bucket);
35+
abstract BiConsumer<SubscriptionContext, CallExpression> visitBucketConstructor();
4036
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.checks.cdk;
21+
22+
import java.util.Collection;
23+
import java.util.Locale;
24+
import java.util.Optional;
25+
import java.util.function.Predicate;
26+
import org.sonar.plugins.python.api.SubscriptionContext;
27+
import org.sonar.plugins.python.api.tree.CallExpression;
28+
import org.sonar.plugins.python.api.tree.Expression;
29+
import org.sonar.plugins.python.api.tree.Token;
30+
import org.sonar.plugins.python.api.tree.Tree;
31+
import org.sonar.python.tree.TreeUtils;
32+
33+
import static org.sonar.python.checks.cdk.CdkUtils.getArgument;
34+
35+
public class CdkPredicate {
36+
37+
private CdkPredicate() {
38+
39+
}
40+
41+
/**
42+
* @return Predicate which tests if expression is boolean literal and is set to `false`
43+
*/
44+
public static Predicate<Expression> isFalse() {
45+
return expression -> Optional.ofNullable(expression.firstToken()).map(Token::value).filter("False"::equals).isPresent();
46+
}
47+
48+
/**
49+
* @return Predicate which tests if expression is `none`
50+
*/
51+
public static Predicate<Expression> isNone() {
52+
return expression -> expression.is(Tree.Kind.NONE);
53+
}
54+
55+
/**
56+
* @return Predicate which tests if expression is a fully qualified name (FQN) and is equal the expected FQN
57+
*/
58+
public static Predicate<Expression> isFqn(String fqnValue) {
59+
return expression -> Optional.ofNullable(TreeUtils.fullyQualifiedNameFromExpression(expression))
60+
.filter(fqnValue::equals)
61+
.isPresent();
62+
}
63+
64+
/**
65+
* @return Predicate which tests if expression is a fully qualified name (FQN) and part of the FQN list
66+
*/
67+
public static Predicate<Expression> isFqnOf(Collection<String> fqnValues) {
68+
return expression -> Optional.ofNullable(TreeUtils.fullyQualifiedNameFromExpression(expression))
69+
.filter(fqnValues::contains)
70+
.isPresent();
71+
}
72+
73+
/**
74+
* @return Predicate which tests if expression is a string and is equal the expected value
75+
*/
76+
public static Predicate<Expression> isString(String expectedValue) {
77+
return expression -> CdkUtils.getString(expression).filter(expectedValue::equals).isPresent();
78+
}
79+
80+
/**
81+
* @return Predicate which tests if expression is a string and starts with the expected value
82+
*/
83+
public static Predicate<Expression> startsWith(String expected) {
84+
return expression -> CdkUtils.getString(expression).filter(str -> str.toLowerCase(Locale.ROOT).startsWith(expected)).isPresent();
85+
}
86+
87+
// TODO refactor this overloaded predicate method
88+
// FIXME we should not raise on a FQN which is not a method call when we want to check for a sensitive method call
89+
public static Predicate<Expression> isSensitiveMethod(SubscriptionContext ctx, String methodFqn, String argName, Predicate<Expression> sensitiveValuePredicate) {
90+
return expression -> {
91+
if (!isFqn(methodFqn).test(expression)) {
92+
return false;
93+
}
94+
if (!expression.is(Tree.Kind.CALL_EXPR)) {
95+
return true;
96+
}
97+
98+
Optional<CdkUtils.ExpressionTrace> argTrace = getArgument(ctx, (CallExpression) expression, argName);
99+
if (argTrace.isEmpty()) {
100+
return true;
101+
}
102+
103+
return argTrace.filter(trace -> trace.hasExpression(sensitiveValuePredicate)).isPresent();
104+
};
105+
}
106+
107+
}

0 commit comments

Comments
 (0)