Skip to content

Commit 5f85f13

Browse files
SONARJAVA-5149 S1075: find when uri is short and relative and stop raising issues (#4961)
1 parent def406e commit 5f85f13

File tree

5 files changed

+174
-27
lines changed

5 files changed

+174
-27
lines changed

its/ruling/src/test/resources/sonar-server/java-S1075.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
{
2-
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/authentication/AuthenticationError.java": [
3-
35
4-
],
52
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/authentication/AuthenticationFilter.java": [
63
34
74
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package checks;
2+
3+
public class HardcodedURICheckSample {
4+
5+
String path = "/home/path/to/my/file.txt"; // Noncompliant
6+
7+
String aVarPath = "/home/path/to/my/file.txt"; // FN, missing semantics
8+
@MyAnnotation(aVarPath = "")
9+
int x = 0;
10+
}

java-checks-test-sources/default/src/main/java/checks/HardcodedURICheckSample.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class HardcodedURICheckSample {
88

99
public static @interface MyAnnotation {
1010
String stuff() default "none";
11+
// we cannot name method path otherwise path is detected as an identifier used in annotation
12+
//, and it creates clashes (FN) with path variables or fields
1113
String path() default "/";
1214
}
1315

@@ -62,4 +64,30 @@ void foo(String s, String var) throws URISyntaxException {
6264

6365
String v1 = s + "//" + s; // Compliant - not a file name
6466
}
67+
68+
@interface MyAnnotation2 {
69+
String aVar() default "";
70+
}
71+
72+
static final String relativePath1 = "/search"; // Compliant, we don't raise issues on short relative uri in constants
73+
static final String relativePath2 = "/group/members";
74+
static final String longRelativePath = "/group/members/list.json"; // Noncompliant
75+
static final String urlPath = "https://www.mywebsite.com"; // Noncompliant
76+
final String staticIsMissingPath = "/search"; // Noncompliant
77+
static String finalIsMissingPath = "/search"; // Noncompliant
78+
79+
static final String default_uri_path = "/a-great/path/for-this-example"; // Compliant, default_uri is constant and is used in an annotation
80+
String aVarPath = "/a-great/path/for-this-example"; // Noncompliant
81+
82+
@MyAnnotation2(aVar = default_uri_path)
83+
void annotated(){}
84+
85+
@MyAnnotation2()
86+
String endpoint_url_path = "/a-great/path/for-this-example"; // Compliant, an annotation is applied on the variable
87+
88+
void reachFullCoverage(){
89+
int path = 0;
90+
path = 10;
91+
}
92+
6593
}

java-checks/src/main/java/org/sonar/java/checks/HardcodedURICheck.java

Lines changed: 127 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,33 @@
1616
*/
1717
package org.sonar.java.checks;
1818

19+
import java.util.ArrayDeque;
20+
import java.util.ArrayList;
1921
import java.util.Arrays;
22+
import java.util.Deque;
23+
import java.util.HashSet;
2024
import java.util.List;
25+
import java.util.Set;
2126
import java.util.regex.Pattern;
2227
import javax.annotation.CheckForNull;
2328
import javax.annotation.Nullable;
2429
import org.sonar.check.Rule;
2530
import org.sonar.java.model.ExpressionUtils;
2631
import org.sonar.java.model.LiteralUtils;
32+
import org.sonar.java.model.ModifiersUtils;
2733
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
34+
import org.sonar.plugins.java.api.JavaFileScannerContext;
2835
import org.sonar.plugins.java.api.semantic.MethodMatchers;
36+
import org.sonar.plugins.java.api.semantic.Symbol;
37+
import org.sonar.plugins.java.api.tree.AnnotationTree;
2938
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
3039
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
3140
import org.sonar.plugins.java.api.tree.BinaryExpressionTree;
3241
import org.sonar.plugins.java.api.tree.ExpressionTree;
3342
import org.sonar.plugins.java.api.tree.IdentifierTree;
3443
import org.sonar.plugins.java.api.tree.LiteralTree;
3544
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
45+
import org.sonar.plugins.java.api.tree.Modifier;
3646
import org.sonar.plugins.java.api.tree.NewClassTree;
3747
import org.sonar.plugins.java.api.tree.Tree;
3848
import org.sonar.plugins.java.api.tree.VariableTree;
@@ -67,20 +77,86 @@ public class HardcodedURICheck extends IssuableSubscriptionVisitor {
6777
private static final Pattern URI_PATTERN = Pattern.compile(URI_REGEX + "|" + LOCAL_URI + "|" + DISK_URI + "|" + BACKSLASH_LOCAL_URI);
6878
private static final Pattern VARIABLE_NAME_PATTERN = Pattern.compile("filename|path", Pattern.CASE_INSENSITIVE);
6979
private static final Pattern PATH_DELIMETERS_PATTERN = Pattern.compile("\"/\"|\"//\"|\"\\\\\\\\\"|\"\\\\\\\\\\\\\\\\\"");
80+
private static final Pattern RELATIVE_URI_PATTERN = Pattern.compile("^(/[\\w-+!*.]+){1,2}");
81+
82+
83+
// we use these variables to track when we are visiting an annotation
84+
private final Deque<AnnotationTree> annotationsStack = new ArrayDeque<>();
85+
86+
private record IdentifierData(Symbol symbol, String identifier) {
87+
}
88+
89+
private final List<IdentifierData> identifiersUsedInAnnotations = new ArrayList<>();
90+
91+
private record VariableData(Symbol symbol, String identifier, ExpressionTree initializer) {
92+
}
93+
94+
private final List<VariableData> hardCodedUri = new ArrayList<>();
95+
96+
@Override
97+
public void setContext(JavaFileScannerContext context) {
98+
super.setContext(context);
99+
annotationsStack.clear();
100+
identifiersUsedInAnnotations.clear();
101+
hardCodedUri.clear();
102+
}
103+
104+
@Override
105+
public void leaveFile(JavaFileScannerContext context) {
106+
// now, we know all variable that are used in annotation so we can report issues
107+
Set<Symbol> idSymbols = new HashSet<>();
108+
Set<String> idNamesWithSemantic = new HashSet<>();
109+
Set<String> idNamesWithoutSemantic = new HashSet<>();
110+
111+
for (IdentifierData i : identifiersUsedInAnnotations) {
112+
if (i.symbol().isUnknown()) {
113+
idNamesWithoutSemantic.add(i.identifier());
114+
} else {
115+
idSymbols.add(i.symbol());
116+
idNamesWithSemantic.add(i.identifier());
117+
}
118+
}
119+
120+
for(VariableData v : hardCodedUri) {
121+
// equals to an identifier with unknown semantic, we cannot compare their symbols
122+
if (idNamesWithoutSemantic.contains(v.identifier())) {
123+
continue;
124+
}
125+
126+
// idNamesWithSemantic is used to only compare the symbols when their string identifier are the same
127+
// as comparing symbols is costly
128+
if (idNamesWithSemantic.contains(v.identifier()) && idSymbols.contains(v.symbol())) {
129+
continue;
130+
}
131+
reportHardcodedURI(v.initializer());
132+
}
133+
}
134+
70135

71136
@Override
72137
public List<Tree.Kind> nodesToVisit() {
73-
return Arrays.asList(Tree.Kind.NEW_CLASS, Tree.Kind.VARIABLE, Tree.Kind.ASSIGNMENT);
138+
return Arrays.asList(Tree.Kind.NEW_CLASS, Tree.Kind.VARIABLE, Tree.Kind.ASSIGNMENT, Tree.Kind.ANNOTATION, Tree.Kind.IDENTIFIER);
74139
}
75140

76141
@Override
77142
public void visitNode(Tree tree) {
78-
if (tree.is(Tree.Kind.NEW_CLASS)) {
79-
checkNewClassTree((NewClassTree) tree);
80-
} else if (tree.is(Tree.Kind.VARIABLE)) {
81-
checkVariable((VariableTree) tree);
82-
} else {
83-
checkAssignment((AssignmentExpressionTree) tree);
143+
if (tree instanceof NewClassTree classTree) {
144+
checkNewClassTree(classTree);
145+
} else if (tree instanceof VariableTree variableTree) {
146+
checkVariable(variableTree);
147+
} else if (tree instanceof AnnotationTree annotationTree) {
148+
annotationsStack.add(annotationTree);
149+
} else if (tree instanceof IdentifierTree identifier && !annotationsStack.isEmpty()) {
150+
identifiersUsedInAnnotations.add(new IdentifierData(identifier.symbol(), identifier.name()));
151+
} else if (tree instanceof AssignmentExpressionTree assignment) {
152+
checkAssignment(assignment);
153+
}
154+
}
155+
156+
@Override
157+
public void leaveNode(Tree tree) {
158+
if (tree instanceof AnnotationTree) {
159+
annotationsStack.pop();
84160
}
85161
}
86162

@@ -91,8 +167,31 @@ private void checkNewClassTree(NewClassTree nct) {
91167
}
92168

93169
private void checkVariable(VariableTree tree) {
94-
if (isFileNameVariable(tree.simpleName())) {
95-
checkExpression(tree.initializer());
170+
ExpressionTree initializer = tree.initializer();
171+
172+
if (!isFileNameVariable(tree.simpleName())
173+
|| initializer == null
174+
// we don't raise issues when the variable is annotated
175+
|| !tree.modifiers().annotations().isEmpty()
176+
) {
177+
return;
178+
}
179+
180+
String stringLiteral = stringLiteral(initializer);
181+
if (stringLiteral == null) {
182+
return;
183+
}
184+
185+
// small relative Uri that are static and final are allowed
186+
if (ModifiersUtils.hasAll(tree.modifiers(), Modifier.STATIC, Modifier.FINAL)
187+
&& RELATIVE_URI_PATTERN.matcher(stringLiteral).matches()) {
188+
return;
189+
}
190+
191+
if (isHardcodedURI(initializer)) {
192+
hardCodedUri.add(new VariableData(tree.symbol(),
193+
tree.simpleName().name(),
194+
initializer));
96195
}
97196
}
98197

@@ -117,26 +216,30 @@ private static boolean isFileNameVariable(@Nullable IdentifierTree variable) {
117216
return variable != null && VARIABLE_NAME_PATTERN.matcher(variable.name()).find();
118217
}
119218

120-
private void checkExpression(@Nullable ExpressionTree expr) {
121-
if (expr != null) {
122-
if (isHardcodedURI(expr)) {
123-
reportHardcodedURI(expr);
124-
} else {
125-
reportStringConcatenationWithPathDelimiter(expr);
126-
}
219+
private void checkExpression(ExpressionTree expr) {
220+
if (isHardcodedURI(expr)) {
221+
reportHardcodedURI(expr);
222+
} else {
223+
reportStringConcatenationWithPathDelimiter(expr);
127224
}
128225
}
129226

130227
private static boolean isHardcodedURI(ExpressionTree expr) {
131-
ExpressionTree newExpr = ExpressionUtils.skipParentheses(expr);
132-
if (!newExpr.is(Tree.Kind.STRING_LITERAL)) {
133-
return false;
134-
}
135-
String stringLiteral = LiteralUtils.trimQuotes(((LiteralTree) newExpr).value());
136-
if(stringLiteral.contains("*") || stringLiteral.contains("$")) {
137-
return false;
228+
String stringLiteral = stringLiteral(expr);
229+
return stringLiteral != null
230+
&& !stringLiteral.contains("*")
231+
&& !stringLiteral.contains("$")
232+
&& URI_PATTERN.matcher(stringLiteral).find();
233+
}
234+
235+
@Nullable
236+
private static String stringLiteral(ExpressionTree expr) {
237+
ExpressionTree unquoted = ExpressionUtils.skipParentheses(expr);
238+
239+
if (unquoted instanceof LiteralTree literalTree && literalTree.is(Tree.Kind.STRING_LITERAL)) {
240+
return LiteralUtils.trimQuotes(literalTree.value());
138241
}
139-
return URI_PATTERN.matcher(stringLiteral).find();
242+
return null;
140243
}
141244

142245
private void reportHardcodedURI(ExpressionTree hardcodedURI) {

java-checks/src/test/java/org/sonar/java/checks/HardcodedURICheckTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.sonar.java.checks.verifier.CheckVerifier;
2121

2222
import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
23+
import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath;
2324

2425
class HardcodedURICheckTest {
2526
@Test
@@ -30,4 +31,12 @@ void test() {
3031
.verifyIssues();
3132
}
3233

34+
@Test
35+
void test_without_semantic() {
36+
CheckVerifier.newVerifier()
37+
.onFile(nonCompilingTestSourcesPath("checks/HardcodedURICheckSample.java"))
38+
.withCheck(new HardcodedURICheck())
39+
.verifyIssues();
40+
}
41+
3342
}

0 commit comments

Comments
 (0)