1616 */
1717package org .sonar .java .checks ;
1818
19+ import java .util .ArrayDeque ;
20+ import java .util .ArrayList ;
1921import java .util .Arrays ;
22+ import java .util .Deque ;
23+ import java .util .HashSet ;
2024import java .util .List ;
25+ import java .util .Set ;
2126import java .util .regex .Pattern ;
2227import javax .annotation .CheckForNull ;
2328import javax .annotation .Nullable ;
2429import org .sonar .check .Rule ;
2530import org .sonar .java .model .ExpressionUtils ;
2631import org .sonar .java .model .LiteralUtils ;
32+ import org .sonar .java .model .ModifiersUtils ;
2733import org .sonar .plugins .java .api .IssuableSubscriptionVisitor ;
34+ import org .sonar .plugins .java .api .JavaFileScannerContext ;
2835import 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 ;
2938import org .sonar .plugins .java .api .tree .AssignmentExpressionTree ;
3039import org .sonar .plugins .java .api .tree .BaseTreeVisitor ;
3140import org .sonar .plugins .java .api .tree .BinaryExpressionTree ;
3241import org .sonar .plugins .java .api .tree .ExpressionTree ;
3342import org .sonar .plugins .java .api .tree .IdentifierTree ;
3443import org .sonar .plugins .java .api .tree .LiteralTree ;
3544import org .sonar .plugins .java .api .tree .MemberSelectExpressionTree ;
45+ import org .sonar .plugins .java .api .tree .Modifier ;
3646import org .sonar .plugins .java .api .tree .NewClassTree ;
3747import org .sonar .plugins .java .api .tree .Tree ;
3848import 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 ) {
0 commit comments