You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/06/10 13:39:08 UTC

(groovy) branch master updated: GROOVY-12081: RegexChecker doesn't support String.matches(Pattern|String)

This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 31c92ab18b GROOVY-12081: RegexChecker doesn't support String.matches(Pattern|String)
31c92ab18b is described below

commit 31c92ab18b1e34738eea595649f2e951ac3094bb
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Jun 10 22:54:21 2026 +1000

    GROOVY-12081: RegexChecker doesn't support String.matches(Pattern|String)
---
 .../groovy/groovy/typecheckers/RegexChecker.groovy |  9 ++++++
 .../groovy/typecheckers/RegexCheckerTest.groovy    | 36 ++++++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy
index f105b6af0c..d59168c7cb 100644
--- a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy
+++ b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy
@@ -148,6 +148,9 @@ class RegexChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
                     super.visitMethodCallExpression(call)
                     if (call.objectExpression instanceof ClassExpression) {
                         checkPatternMethod(call, call.objectExpression.type)
+                    } else if (isString(call.receiver) && call.methodAsString == 'matches' && call.arguments.expressions) {
+                        def exp = findConstExp(call.arguments.getExpression(0), String)
+                        checkRegex(exp, call)
                     } else if (isPattern(call.receiver) && call.methodAsString == 'matcher') {
                         def source = call.receiver instanceof VariableExpression ? findTargetVariable(call.receiver) : call.receiver
                         def groupCount = source?.getNodeMetaData(REGEX_GROUP_COUNT)
@@ -277,6 +280,12 @@ class RegexChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
             || exp.getNodeMetaData(StaticTypesMarker.INFERRED_RETURN_TYPE) == MATCHER_TYPE
     }
 
+    private boolean isString(Expression exp) {
+        exp.type == STRING_TYPE
+            || exp.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE) == STRING_TYPE
+            || exp.getNodeMetaData(StaticTypesMarker.INFERRED_RETURN_TYPE) == STRING_TYPE
+    }
+
     private boolean isPattern(Expression exp) {
         exp.type == PATTERN_TYPE
             || exp.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE) == PATTERN_TYPE
diff --git a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/RegexCheckerTest.groovy b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/RegexCheckerTest.groovy
index a28a8dda07..119e2f0994 100644
--- a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/RegexCheckerTest.groovy
+++ b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/RegexCheckerTest.groovy
@@ -241,6 +241,42 @@ final class RegexCheckerTest {
         assertScript shell, "assert Pattern.matches(/\\d{4}-\\d{1,2}-\\d{1,2}/, '2020-12-31')"
     }
 
+    @Test
+    void testBadRegexJavaStringMatches() {
+        // GROOVY-12081: the regex argument of String#matches is now checked
+        def err = shouldFail shell, '''
+            assert 'JOHN'.matches('[A-Z+') // unclosed character class
+        '''
+        assert err.message ==~ /(?s).*Bad regex.*: Unclosed character class.*/
+
+        // String-typed receiver via parameter
+        err = shouldFail shell, '''
+            boolean isUpper(String s) { s.matches('[A-Z+') }
+        '''
+        assert err.message ==~ /(?s).*Bad regex.*: Unclosed character class.*/
+
+        // regex supplied via a local constant variable
+        err = shouldFail shell, '''
+            def re = '[A-Z+'
+            assert 'JOHN'.matches(re)
+        '''
+        assert err.message ==~ /(?s).*Bad regex.*: Unclosed character class.*/
+    }
+
+    @Test
+    void testGoodRegexJavaStringMatches() {
+        assertScript shell, '''
+            boolean isUpper(String s) { s.matches('[A-Z]+') }
+            assert isUpper('JOHN')
+            assert !isUpper('John')
+        '''
+        // a matches(String) on a non-String receiver must not be treated as a regex
+        assertScript shell, '''
+            class Foo { boolean matches(String s) { true } }
+            assert new Foo().matches('[A-Z+')
+        '''
+    }
+
     @Test
     void testBadRegexGroupCount() {
         def shouldFailWithGroupCountError = { String script ->