2424import java .util .HashMap ;
2525import java .util .List ;
2626import java .util .Map ;
27- import java .util .Optional ;
2827import java .util .function .Function ;
28+ import java .util .regex .Matcher ;
29+ import java .util .regex .Pattern ;
2930import java .util .stream .Collectors ;
3031import org .sonar .check .Rule ;
32+ import org .sonar .java .model .ExpressionUtils ;
3133import org .sonar .java .regex .RegexCheck ;
3234import org .sonar .java .regex .RegexParseResult ;
3335import org .sonar .java .regex .ast .BackReferenceTree ;
3436import org .sonar .java .regex .ast .CapturingGroupTree ;
3537import org .sonar .java .regex .ast .RegexBaseVisitor ;
3638import org .sonar .plugins .java .api .semantic .MethodMatchers ;
37- import org .sonar .plugins .java .api .semantic .Type ;
3839import org .sonar .plugins .java .api .tree .ExpressionTree ;
3940import org .sonar .plugins .java .api .tree .MethodInvocationTree ;
4041
@@ -47,13 +48,27 @@ public class UnusedGroupNamesCheck extends AbstractRegexCheckTrackingMatchers {
4748
4849 private static final String JAVA_UTIL_REGEX_MATCHER = "java.util.regex.Matcher" ;
4950
50- private static final MethodMatchers MATCHER_GROUP = MethodMatchers .create ()
51- .ofTypes (JAVA_UTIL_REGEX_MATCHER )
52- .names ("group" )
53- // covers both 'group(String)' and 'group(int)'
54- .addParametersMatcher (JAVA_LANG_STRING )
55- .addParametersMatcher ("int" )
56- .build ();
51+ private static final Pattern GROUP_NUMBER_REPLACEMENT_REGEX = Pattern .compile ("(?<!\\ \\ )\\ $(?<number>\\ d++)" );
52+ private static final Pattern GROUP_NAME_REPLACEMENT_REGEX = Pattern .compile ("(?<!\\ \\ )\\ $\\ {(?<name>[A-Za-z][0-9A-Za-z]*+)\\ }" );
53+
54+ private static final MethodMatchers MATCHER_GROUP = MethodMatchers .or (
55+ MethodMatchers .create ()
56+ .ofTypes (JAVA_UTIL_REGEX_MATCHER )
57+ .names ("group" )
58+ // covers both 'group(String)' and 'group(int)'
59+ .addParametersMatcher (JAVA_LANG_STRING )
60+ .addParametersMatcher ("int" )
61+ .build (),
62+ MethodMatchers .create ()
63+ .ofTypes (JAVA_UTIL_REGEX_MATCHER )
64+ .names ("appendReplacement" )
65+ .addParametersMatcher (MethodMatchers .ANY , MethodMatchers .ANY )
66+ .build (),
67+ MethodMatchers .create ()
68+ .ofTypes (JAVA_UTIL_REGEX_MATCHER )
69+ .names ("replaceAll" , "replaceFirst" )
70+ .addParametersMatcher (MethodMatchers .ANY )
71+ .build ());
5772
5873 @ Override
5974 protected MethodMatchers trackedMethodMatchers () {
@@ -76,38 +91,47 @@ protected void checkRegex(RegexParseResult regexForLiterals, List<MethodInvocati
7691 }
7792
7893 private void checkGroupUsage (MethodInvocationTree mit , KnownGroupsCollector knownGroups ) {
79- ExpressionTree arg0 = mit .arguments ().get (0 );
80- Type arg0Type = arg0 .symbolType ();
81- if (arg0Type .is ("int" )) {
82- checkUsingNumberInsteadOfName (knownGroups , arg0 );
94+ String methodName = ExpressionUtils .methodName (mit ).name ();
95+ if ("group" .equals (methodName )) {
96+ ExpressionTree arg0 = mit .arguments ().get (0 );
97+ if (arg0 .symbolType ().is ("int" )) {
98+ arg0 .asConstant (Integer .class ).ifPresent (index -> checkUsingNumberInsteadOfName (knownGroups , arg0 , index , false ));
99+ } else {
100+ arg0 .asConstant (String .class ).ifPresent (name -> checkNoSuchName (knownGroups , arg0 , name ));
101+ }
83102 } else {
84- checkNoSuchName (knownGroups , arg0 );
103+ int argIndex = "appendReplacement" .equals (methodName ) ? 1 : 0 ;
104+ ExpressionTree arg = mit .arguments ().get (argIndex );
105+ arg .asConstant (String .class ).ifPresent (replacement -> checkUsingReplacementString (knownGroups , arg , replacement ));
85106 }
86107 }
87108
88- private void checkUsingNumberInsteadOfName (KnownGroupsCollector knownGroups , ExpressionTree arg0 ) {
89- Optional <Integer > groupNumber = arg0 .asConstant (Integer .class );
90- if (!groupNumber .isPresent ()) {
91- return ;
109+ private void checkUsingReplacementString (KnownGroupsCollector knownGroups , ExpressionTree arg , String replacement ) {
110+ Matcher indexMatcher = GROUP_NUMBER_REPLACEMENT_REGEX .matcher (replacement );
111+ while (indexMatcher .find ()) {
112+ int groupNumber = Integer .parseInt (indexMatcher .group ("number" ));
113+ checkUsingNumberInsteadOfName (knownGroups , arg , groupNumber , true );
114+ }
115+ Matcher nameMatcher = GROUP_NAME_REPLACEMENT_REGEX .matcher (replacement );
116+ while (nameMatcher .find ()) {
117+ checkNoSuchName (knownGroups , arg , nameMatcher .group ("name" ));
92118 }
93- Integer groupNumberValue = groupNumber .get ();
94- CapturingGroupTree capturingGroupTree = knownGroups .groupsByNumber .get (groupNumberValue );
119+ }
120+
121+ private void checkUsingNumberInsteadOfName (KnownGroupsCollector knownGroups , ExpressionTree arg0 , int groupNumber , boolean dollarReference ) {
122+ CapturingGroupTree capturingGroupTree = knownGroups .groupsByNumber .get (groupNumber );
95123 if (capturingGroupTree == null ) {
96124 return ;
97125 }
98- String message = String .format (ISSUE_USE_NAME_INSTEAD_OF_NUMBER , capturingGroupTree .getName ().orElse ("?" ));
99- RegexIssueLocation secondary = toLocation (capturingGroupTree , "Group %d" , g -> groupNumberValue );
126+ String groupName = capturingGroupTree .getName ().map (name -> dollarReference ? ("${" + name + "}" ) : name ).orElse ("?" );
127+ String message = String .format (ISSUE_USE_NAME_INSTEAD_OF_NUMBER , groupName );
128+ RegexIssueLocation secondary = toLocation (capturingGroupTree , "Group %d" , g -> groupNumber );
100129 reportIssue (arg0 , message , null , Collections .singletonList (secondary ));
101130 }
102131
103- private void checkNoSuchName (KnownGroupsCollector knownGroups , ExpressionTree arg0 ) {
104- Optional <String > groupName = arg0 .asConstant (String .class );
105- if (!groupName .isPresent ()) {
106- return ;
107- }
108- String groupNameValue = groupName .get ();
109- if (!knownGroups .groupsByName .keySet ().contains (groupNameValue )) {
110- String message = String .format (ISSUE_NO_GROUP_WITH_SUCH_NAME , groupNameValue );
132+ private void checkNoSuchName (KnownGroupsCollector knownGroups , ExpressionTree arg0 , String groupName ) {
133+ if (!knownGroups .groupsByName .keySet ().contains (groupName )) {
134+ String message = String .format (ISSUE_NO_GROUP_WITH_SUCH_NAME , groupName );
111135 List <RegexIssueLocation > secondaries = knownGroups .groupsByName .values ()
112136 .stream ()
113137 .map (group -> toLocation (group , "Named group '%s'" , g -> g .getName ().get ()))
0 commit comments