groovy-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From pa...@apache.org
Subject [groovy] branch master updated: GROOVY-9680: Groovy 4 should provide some pre-canned type checker extensions (closes #1345)
Date Wed, 19 Aug 2020 01:39:31 GMT
This is an automated email from the ASF dual-hosted git repository.

paulk 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 8490e9c  GROOVY-9680: Groovy 4 should provide some pre-canned type checker extensions
(closes #1345)
8490e9c is described below

commit 8490e9cd2c85aa076e9df0035b96737c09e31e22
Author: Paul King <paulk@asert.com.au>
AuthorDate: Mon Aug 10 11:01:23 2020 +1000

    GROOVY-9680: Groovy 4 should provide some pre-canned type checker extensions (closes #1345)
---
 gradle/upload.gradle                               |   3 +-
 settings.gradle                                    |   1 +
 subprojects/groovy-typecheckers/build.gradle       |  23 ++
 .../groovy/groovy/typecheckers/RegexChecker.groovy | 294 ++++++++++++++++++++
 .../groovy/typecheckers/CheckingVisitor.groovy     |  63 +++++
 .../groovy/typecheckers/RegexCheckerTest.groovy    | 303 +++++++++++++++++++++
 6 files changed, 686 insertions(+), 1 deletion(-)

diff --git a/gradle/upload.gradle b/gradle/upload.gradle
index ceff62b..02aee67 100644
--- a/gradle/upload.gradle
+++ b/gradle/upload.gradle
@@ -192,7 +192,8 @@ def optionalModules = [
         'groovy-contracts',
         'groovy-jaxb',
         'groovy-macro-library',
-        'groovy-testng'
+        'groovy-testng',
+        'groovy-typecheckers'
 ]
 
 def pomAll = {
diff --git a/settings.gradle b/settings.gradle
index 394acfc..b8a3622 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -55,6 +55,7 @@ def subprojects = ['groovy-ant',
         'groovy-test',
         'groovy-test-junit5',
         'groovy-testng',
+        'groovy-typecheckers',
         'groovy-xml',
         'groovy-yaml',
         'performance',
diff --git a/subprojects/groovy-typecheckers/build.gradle b/subprojects/groovy-typecheckers/build.gradle
new file mode 100644
index 0000000..c1a8be4
--- /dev/null
+++ b/subprojects/groovy-typecheckers/build.gradle
@@ -0,0 +1,23 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed oHistoryRecordGetTextToRunTestsn
an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+dependencies {
+    implementation rootProject
+    testImplementation project(':groovy-test')
+}
diff --git a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy
new file mode 100644
index 0000000..62afb3e
--- /dev/null
+++ b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy
@@ -0,0 +1,294 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.typecheckers
+
+import org.apache.groovy.lang.annotation.Incubating
+import org.apache.groovy.typecheckers.CheckingVisitor
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.expr.BinaryExpression
+import org.codehaus.groovy.ast.expr.BitwiseNegationExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.DeclarationExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.MethodCall
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
+import org.codehaus.groovy.syntax.Types
+import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
+import org.codehaus.groovy.transform.stc.StaticTypesMarker
+
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+import java.util.regex.PatternSyntaxException
+
+import static org.codehaus.groovy.ast.ClassHelper.PATTERN_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE
+import static org.codehaus.groovy.syntax.Types.isAssignment
+import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.checkCompatibleAssignmentTypes
+
+/**
+ * Checks at compile-time for cases of invalid regex usage where the actual regex string
can be identified
+ * (e.g. inline or defined by a local variable with an initial value or a field with an initial
value).
+ * A number of errors which would normally surface only at runtime are handled.
+ * <ul>
+ * <li>
+ * Invalid pattern definitions when using the Groovy pattern operator or the JDK's {@code
Pattern#compile} method:
+ * <pre>
+ *     ~/\w{3/               // missing closing repetition quantifier brace
+ *     ~"(.)o(.*"            // missing closing group bracket
+ *     Pattern.compile(/?/)  // dangling meta character '?' (Java longhand)
+ * </pre>
+ * These examples illustrate explicitly defined constant strings but local variable
+ * or field definitions where a constant string can be determined are also checked.
+ * </li>
+ * <li>
+ * Invalid regex strings in conjunction with Groovy's regex find and regex match expressions,
or the JDK's {@code Pattern#matches} method:
+ * <pre>
+ *     'foobar'  =~ /f[o]{2/        // missing closing repetition quantifier brace
+ *     'foobar' ==~ /(foo/          // missing closing group bracket
+ *     Pattern.matches(/?/, 'foo')  // dangling meta character '?' (Java longhand)
+ * </pre>
+ * </li>
+ * Invalid group count terms where the regex string can be determined and the group index
can
+ * be determined to be a constant.
+ * <li>
+ * <pre>
+ *     def m = 'foobar' =~ /(...)(...)/
+ *     assert m[0][1] == 'foo'     // okay
+ *     assert m[0][3]              // type error: only two groups in regex
+ * </pre>
+ * And similarly for Java long-hand variants:
+ * <pre>
+ *     Pattern p = Pattern.compile('(...)(...)')
+ *     Matcher m = p.matcher('foobar')
+ *     assert m.find()
+ *     assert m.group(1) == 'foo'  // okay
+ *     assert m.group(3)           // type error: only two groups in regex
+ * </pre>
+ * </li>
+ * </ul>
+ *
+ * Also, when using the regex find operator, smarter type inferencing occurs.
+ * For an expression like {@code matcher[0]}, the {@code getAt} extension method for {@code
Matcher}
+ * is called. This may return a String (if no groups occur within the regex) or a list of
String values
+ * if grouping is used, hence the declared return type of the mentioned {@code getAt} method
is {@code Object}
+ * to account for these two possibilities. When using {@code RegexChecker}, the inferred
type will be either
+ * {@code String} or {@code List&lt;String&gt;} when a regex string can be identified.
+ *
+ * Over time, the idea would be to support more cases as per:
+ * https://checkerframework.org/manual/#regex-checker
+ * https://homes.cs.washington.edu/~mernst/pubs/regex-types-ftfjp2012.pdf
+ * https://github.com/typetools/checker-framework/tree/master/checker/src/main/java/org/checkerframework/checker/regex
+ */
+@Incubating
+class RegexChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
+    private static final String REGEX_GROUP_COUNT = RegexChecker.simpleName + "_INFERRED_GROUP_COUNT"
+    private static final String REGEX_MATCHER_RESULT_TYPE = RegexChecker.simpleName + "_MATCHER_RESULT_INFERRED_TYPE"
+    private static final ClassNode MATCHER_TYPE = ClassHelper.make(Matcher)
+
+    @Override
+    Object run() {
+        beforeVisitMethod { MethodNode method ->
+            def visitor = new CheckingVisitor() {
+                @Override
+                void visitBitwiseNegationExpression(BitwiseNegationExpression expression)
{
+                    super.visitBitwiseNegationExpression(expression)
+                    def exp = findConstExp(expression.expression, String)
+                    checkRegex(exp, expression)
+                }
+
+                @Override
+                void visitBinaryExpression(BinaryExpression expression) {
+                    super.visitBinaryExpression(expression)
+                    if (expression.operation.type in [Types.FIND_REGEX, Types.MATCH_REGEX])
{
+                        def exp = findConstExp(expression.rightExpression, String)
+                        checkRegex(exp, expression)
+                    } else if (expression.operation.type == Types.LEFT_SQUARE_BRACKET) {
+                        if (isVariableExpression(expression.leftExpression)) {
+                            def var = findTargetVariable(expression.leftExpression)
+                            def groupCount = var?.getNodeMetaData(REGEX_GROUP_COUNT)
+                            if (groupCount != null) {
+                                expression.putNodeMetaData(REGEX_GROUP_COUNT, groupCount)
+                                if (groupCount == 0) {
+                                    expression.putNodeMetaData(REGEX_MATCHER_RESULT_TYPE,
STRING_TYPE)
+                                } else {
+                                    expression.putNodeMetaData(REGEX_MATCHER_RESULT_TYPE,
buildListType(STRING_TYPE.plainNodeReference))
+                                }
+                            }
+                        }
+                    }
+                }
+
+                @Override
+                void visitMethodCallExpression(MethodCallExpression call) {
+                    super.visitMethodCallExpression(call)
+                    if (isClassExpression(call.objectExpression)) {
+                        checkPatternMethod(call, call.objectExpression.type)
+                    } else if (isPattern(call.receiver) && call.methodAsString ==
'matcher') {
+                        def var = findTargetVariable(call.receiver)
+                        def groupCount = var?.getNodeMetaData(REGEX_GROUP_COUNT)
+                        if (groupCount != null) {
+                            call.putNodeMetaData(REGEX_GROUP_COUNT, groupCount)
+                        }
+                    }
+                }
+
+                @Override
+                void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
+                    super.visitStaticMethodCallExpression(call)
+                    checkPatternMethod(call, call.ownerType)
+                }
+
+                private void checkPatternMethod(MethodCall call, ClassNode classType) {
+                    def arguments = call.arguments
+                    if (classType == PATTERN_TYPE && call.methodAsString in ['compile',
'matches'] && arguments.expressions) {
+                        def exp = findConstExp(arguments.getExpression(0), String)
+                        checkRegex(exp, call)
+                    }
+                }
+
+                @Override
+                void visitDeclarationExpression(DeclarationExpression decl) {
+                    super.visitDeclarationExpression(decl)
+                    if (decl.variableExpression != null) {
+                        if (isConstantExpression(decl.rightExpression)) {
+                            localConstVars.put(decl.variableExpression, decl.rightExpression)
+                        }
+                        def groupCount = decl.rightExpression.getNodeMetaData(REGEX_GROUP_COUNT)
+                        if (groupCount != null) {
+                            decl.variableExpression.putNodeMetaData(REGEX_GROUP_COUNT, groupCount)
+                        }
+                    }
+                }
+
+            }
+            method.code.visit(visitor)
+        }
+
+        incompatibleAssignment { lhsType, rhsType, expr ->
+            if (isBinaryExpression(expr) && isAssignment(expr.operation.type)) {
+                def from = expr.rightExpression
+                if (isBinaryExpression(from) && from.operation.type == Types.LEFT_SQUARE_BRACKET
&& getType(from.leftExpression) == MATCHER_TYPE) {
+                    ClassNode inferred = from.getNodeMetaData(REGEX_MATCHER_RESULT_TYPE)
+                    if (inferred) {
+                        handled = true
+                        if (checkCompatibleAssignmentTypes(lhsType, inferred, from)) {
+                            storeType(expr, inferred)
+                        } else {
+                            addStaticTypeError('Cannot assign value of type ' + inferred
+ ' to variable of type ' + lhsType, expr)
+                        }
+                    }
+                }
+            }
+        }
+
+        methodNotFound { receiverType, name, argList, argTypes, call ->
+            def receiver = call.receiver
+            if (isBinaryExpression(receiver) && receiver.operation.type == Types.LEFT_SQUARE_BRACKET
&& getType(receiver.leftExpression) == MATCHER_TYPE) {
+                ClassNode inferred = receiver.getNodeMetaData(REGEX_MATCHER_RESULT_TYPE)
+                if (inferred) {
+                    makeDynamic(call, inferred)
+                }
+            }
+        }
+
+        afterVisitMethod { MethodNode method ->
+            def visitor = new CheckingVisitor() {
+                @Override
+                void visitDeclarationExpression(DeclarationExpression decl) {
+                    super.visitDeclarationExpression(decl)
+                    if (decl.variableExpression != null) {
+                        if (isConstantExpression(decl.rightExpression)) {
+                            localConstVars.put(decl.variableExpression, decl.rightExpression)
+                        }
+                        def groupCount = decl.rightExpression.getNodeMetaData(REGEX_GROUP_COUNT)
+                        if (groupCount != null) {
+                            decl.variableExpression.putNodeMetaData(REGEX_GROUP_COUNT, groupCount)
+                        }
+                    }
+                }
+
+                @Override
+                void visitMethodCallExpression(MethodCallExpression call) {
+                    if (isPattern(call.receiver) && call.methodAsString == 'matcher')
{
+                        def var = findTargetVariable(call.receiver)
+                        def groupCount = var?.getNodeMetaData(REGEX_GROUP_COUNT)
+                        if (groupCount != null) {
+                            call.putNodeMetaData(REGEX_GROUP_COUNT, groupCount)
+                        }
+                    }
+                    super.visitMethodCallExpression(call)
+                    if (isVariableExpression(call.objectExpression) && call.methodAsString
== 'group' && isMatcher(call.receiver) && call.arguments.expressions) {
+                        def var = findTargetVariable(call.receiver)
+                        def maxCnt = var?.getNodeMetaData(REGEX_GROUP_COUNT)
+                        if (maxCnt != null) {
+                            def cnt = findConstExp(call.arguments.getExpression(0), Integer).value
+                            if (cnt > maxCnt) {
+                                addStaticTypeError("Invalid group count " + cnt + " for regex
with " + maxCnt + " group" + (maxCnt == 1 ? "" : "s"), call)
+                            }
+                        }
+                    }
+                }
+
+                @Override
+                void visitBinaryExpression(BinaryExpression expression) {
+                    super.visitBinaryExpression(expression)
+                    if (expression.operation.type == Types.LEFT_SQUARE_BRACKET) {
+                        def maxCnt = expression.leftExpression?.getNodeMetaData(REGEX_GROUP_COUNT)
+                        if (maxCnt != null) {
+                            def cnt = findConstExp(expression.rightExpression, Integer).value
+                            if (cnt > maxCnt) {
+                                addStaticTypeError("Invalid group count " + cnt + " for regex
with " + maxCnt + " group" + (maxCnt == 1 ? "" : "s"), expression)
+                            }
+                        }
+                    }
+                }
+            }
+            method.code.visit(visitor)
+        }
+    }
+
+    private boolean isMatcher(Expression obj) {
+        obj.type == MATCHER_TYPE ||
+                obj.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE) == MATCHER_TYPE ||
+                obj.getNodeMetaData(StaticTypesMarker.INFERRED_RETURN_TYPE) == MATCHER_TYPE
+    }
+
+    private boolean isPattern(Expression obj) {
+        obj.type == PATTERN_TYPE ||
+                obj.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE) == PATTERN_TYPE ||
+                obj.getNodeMetaData(StaticTypesMarker.INFERRED_RETURN_TYPE) == PATTERN_TYPE
+    }
+
+    private void checkRegex(ConstantExpression regex, Expression target) {
+        if (regex == null) return
+        try {
+            def pattern = Pattern.compile(regex.value)
+            Matcher m = pattern.matcher('')
+            target.putNodeMetaData(REGEX_GROUP_COUNT, m.groupCount())
+        } catch (PatternSyntaxException ex) {
+            String additional = regex.lineNumber != target.lineNumber ?
+                " @ line $regex.lineNumber, column $regex.columnNumber: " : ": "
+            addStaticTypeError("Bad regex" + additional + ex.message, target)
+        }
+    }
+
+}
diff --git a/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/CheckingVisitor.groovy
b/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/CheckingVisitor.groovy
new file mode 100644
index 0000000..c58a2e2
--- /dev/null
+++ b/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/CheckingVisitor.groovy
@@ -0,0 +1,63 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.typecheckers
+
+import org.codehaus.groovy.ast.ClassCodeVisitorSupport
+import org.codehaus.groovy.ast.FieldNode
+import org.codehaus.groovy.ast.Variable
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.VariableExpression
+import org.codehaus.groovy.control.SourceUnit
+
+class CheckingVisitor extends ClassCodeVisitorSupport {
+    protected final Map<Expression, Expression> localConstVars = new HashMap<>()
+
+    protected Expression findConstExp(Expression exp, Class type) {
+        if (exp instanceof ConstantExpression && exp.value.getClass().isAssignableFrom(type))
{
+            return exp
+        }
+        if (exp instanceof VariableExpression) {
+            def var = findTargetVariable(exp)
+            if (var instanceof FieldNode && var.hasInitialExpression()) {
+                return findConstExp(var.initialExpression, type)
+            }
+            if (localConstVars.containsKey(var)) {
+                return findConstExp(localConstVars.get(var), type)
+            }
+        }
+        return null
+    }
+
+    @Override
+    protected SourceUnit getSourceUnit() {
+        return null
+    }
+
+    static Variable findTargetVariable(final VariableExpression ve) {
+        Variable accessedVariable = ve.getAccessedVariable()
+        if (accessedVariable != null && accessedVariable != ve) {
+            if (accessedVariable instanceof VariableExpression) {
+                return findTargetVariable((VariableExpression) accessedVariable)
+            }
+            return accessedVariable
+        }
+        return ve
+    }
+}
diff --git a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/RegexCheckerTest.groovy
b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/RegexCheckerTest.groovy
new file mode 100644
index 0000000..138b1bd
--- /dev/null
+++ b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/RegexCheckerTest.groovy
@@ -0,0 +1,303 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.typecheckers
+
+import groovy.transform.TypeChecked
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
+import org.codehaus.groovy.control.customizers.ImportCustomizer
+import org.junit.BeforeClass
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+class RegexCheckerTest {
+    private static GroovyShell shell
+
+    @BeforeClass
+    static void setup() {
+        def cc = new CompilerConfiguration()
+        def customizer = new ASTTransformationCustomizer(TypeChecked)
+        customizer.annotationParameters = [extensions: 'groovy.typecheckers.RegexChecker']
+        cc.addCompilationCustomizers(customizer)
+        cc.addCompilationCustomizers(new ImportCustomizer().addStarImports("java.util.regex"))
+        shell = new GroovyShell(cc)
+    }
+
+    @Test
+    void testBadRegexForExplicitPatternDeclaration() {
+        def err = shouldFail(shell, '''
+        def shortNamed = ~/\\w{3/ // missing closing quantifier brace
+        ''')
+        assert err.message.contains(/Bad regex: Unclosed counted closure/)
+
+        err = shouldFail(shell, '''
+        def oNamed = ~/(.)o(.*/ // missing closing grouping bracket
+        ''')
+        assert err.message.contains('Bad regex: Unclosed group')
+    }
+
+    @Test
+    void testGoodRegexForExplicitPatternDeclaration() {
+        assertScript shell, '''
+        def pets = ['cat', 'dog', 'goldfish']
+        def shortNamed = ~/\\w{3}/
+        assert pets.grep(shortNamed) == ['cat', 'dog']
+        '''
+
+        assertScript shell, '''
+        def pets = ['cat', 'dog', 'goldfish']
+        def oNamed = ~/(.)o(.*)/ // missing closing bracket
+        assert pets.grep(oNamed) == ['dog', 'goldfish']
+        '''
+    }
+
+    @Test
+    void testBadRegexForExplicitJavaPatternDeclaration() {
+        def err = shouldFail(shell, '''
+        Pattern.compile(/?/) // dangling metacharacter
+        ''')
+        assert err.message.contains(/Bad regex: Dangling meta character '?'/)
+    }
+
+    @Test
+    void testGoodRegexForExplicitJavaPatternDeclaration() {
+        assertScript shell, '''
+        def pets = ['bird', 'cat', 'goldfish']
+        def threeOrFour = Pattern.compile(/....?/)
+        assert pets.grep(threeOrFour) == ['bird', 'cat']
+        '''
+    }
+
+    @Test
+    void testBadRegexWithRegexMatchOperator() {
+        // explicit string cases
+        def err = shouldFail(shell, '''
+        def newYears = '2020-12-31'
+        def matcher = newYears =~ /(\\d{4})-(\\d{1,2})-(\\d{1,2}/
+        ''')
+        assert err.message.contains(/Bad regex: Unclosed group/)
+
+        err = shouldFail(shell, '''
+        def newYears = '2020-12-31'
+        def matcher = newYears =~ /(\\d{4})-(\\d{1,2})-(\\d{1,2)/
+        ''')
+        assert err.message.contains(/Bad regex: Unclosed counted closure/)
+
+        // field case(s)
+        err = shouldFail(shell, '''
+        class Foo {
+            private static final REGEX = /(\\d{4})-(\\d{1,2})-(\\d{1,2}/
+            static void main(String[] args) {
+                def newYears = '2020-12-31'
+                def m = newYears =~ REGEX
+            }
+        }
+        ''')
+        assert err.message ==~ /(?s).*Bad regex.*: Unclosed group.*/
+
+        // local var case(s)
+        err = shouldFail(shell, $/
+        class Foo {
+            static void main(String[] args) {
+                def newYears = '2020-12-31'
+                def REGEX = /(\d{4})-(\d{1,2})-(\d{1,2}/
+                def m = newYears =~ REGEX
+            }
+        }
+        /$)
+        assert err.message ==~ /(?s).*Bad regex.*: Unclosed group.*/
+    }
+
+    @Test
+    void testGoodRegexWithRegexMatchOperator() {
+        // explicit cases
+        assertScript shell, '''
+        def newYears = '2020-12-31'
+        Matcher m = newYears =~ /(\\d{4})-(\\d{1,2})-(\\d{1,2})/
+        List parts = (List) m[0]
+        assert parts[1] == '2020'
+        assert parts[2] == '12'
+        assert parts[3] == '31'
+        '''
+        // as above without cast
+        assertScript shell, '''
+        def newYears = '2020-12-31'
+        Matcher m = newYears =~ /(\\d{4})-(\\d{2})-(\\d{2})/
+        List parts = m[0]
+        assert parts[1] == '2020'
+        assert parts[2] == '12'
+        assert parts[3] == '31'
+        '''
+
+        // field
+        assertScript shell, '''
+        class Foo {
+            private static final REGEX = /(\\d{4})-(\\d{1,2})-(\\d{1,2})/
+            static void main(String[] args) {
+                def newYears = '2020-12-31'
+                Matcher m = newYears =~ REGEX
+                List parts = (List) m[0]
+                assert parts[1] == '2020'
+                assert parts[2] == '12'
+                assert parts[3] == '31'
+            }
+        }
+        '''
+
+        // local var cases
+        assertScript shell, '''
+        def newYears = '2020-12-31'
+        def REGEX = /(\\d{4})-(\\d{1,2})-(\\d{1,2})/
+        Matcher m = newYears =~ REGEX
+        List parts = (List) m[0]
+        assert parts[1] == '2020'
+        assert parts[2] == '12'
+        assert parts[3] == '31'
+        '''
+        // as above without groups or casting
+        assertScript shell, '''
+        def newYears = '2020-12-31'
+        Matcher m = newYears =~ /\\d{2}/
+        assert m[0].toUpperCase() == '20'
+        assert m[1].getBytes() == [50, 48]
+        assert m[2] == '12'
+        assert m[3] == '31'
+        '''
+    }
+
+    @Test
+    void testBadRegexWithRegexFindOperator() {
+        // explicit string cases
+        def err = shouldFail(shell, $/
+        def newYears = '2020-12-31'
+        assert newYears ==~ /\d{4}-\d{1,2}-\d{1,2/
+        /$)
+        assert err.message.contains(/Bad regex: Unclosed counted closure/)
+
+        // field case(s)
+        err = shouldFail(shell, '''
+        class Foo {
+            private static final REGEX = /2020-12-[0123][0123456789/
+            static void main(String[] args) {
+                def newYears = '2020-12-31'
+                assert newYears ==~ REGEX
+            }
+        }
+        ''')
+        assert err.message ==~ /(?s).*Bad regex.*: Unclosed character class.*/
+
+        // local var case(s)
+        err = shouldFail(shell, '''
+        def REGEX = /?/
+        def newYears = '2020-12-31'
+        assert newYears ==~ REGEX
+        ''')
+        assert err.message ==~ /(?s).*Bad regex.*: Dangling meta character '?'.*/
+    }
+
+    @Test
+    void testGoodRegexWithRegexFindOperator() {
+        // field case(s)
+        assertScript shell, '''
+        class Foo {
+            private static final REGEX = /\\d{4}-\\d{1,2}-\\d{1,2}/
+            static void main(String[] args) {
+                def newYears = '2020-12-31'
+                assert newYears ==~ REGEX
+            }
+        }
+        '''
+    }
+
+    @Test
+    void testBadRegexJavaMatches() {
+        // local var
+        def err = shouldFail(shell, '''
+        def REGEX = /?/
+        Pattern.matches(REGEX, 'foo')
+        ''')
+        assert err.message ==~ /(?s).*Bad regex.*: Dangling meta character '?'.*/
+    }
+
+    @Test
+    void testGoodRegexJavaMatches() {
+        assertScript shell, "assert Pattern.matches(/\\d{4}-\\d{1,2}-\\d{1,2}/, '2020-12-31')"
+    }
+
+    @Test
+    void testBadRegexGroupCount() {
+        shouldFailWithBadGroupCount'''
+        def m = 'foobaz' =~ /(...)(...)/
+        assert m.find()
+        assert m.group(3)
+        '''
+        shouldFailWithBadGroupCount'''
+        def p = ~'(...)(...)'
+        Matcher m = p.matcher('barfoo')
+        assert m.find()
+        assert m.group(3)
+        '''
+        shouldFailWithBadGroupCount'''
+        Pattern p = Pattern.compile('(...)(...)')
+        Matcher m = p.matcher('foobar')
+        assert m.find()
+        assert m.group(3)
+        '''
+        shouldFailWithBadGroupCount'''
+        def m = 'barbaz' =~ /(...)(...)/
+        assert m[0][3]
+        '''
+    }
+
+    private void shouldFailWithBadGroupCount(String script) {
+        def err = shouldFail(shell, script)
+        assert err.message.contains(/Invalid group count 3 for regex with 2 groups/)
+    }
+
+    @Test
+    void testGoodRegexGroupCount() {
+        assertScript shell, '''
+        def m = 'foobaz' =~ /(...)(...)/
+        assert m.find()
+        assert m.group(1) == 'foo'
+        assert m.group(2) == 'baz'
+        '''
+        assertScript shell, '''
+        def p = ~'(...)(...)'
+        Matcher m = p.matcher('barfoo')
+        assert m.find()
+        assert m.group(1) == 'bar'
+        assert m.group(2) == 'foo'
+        '''
+        assertScript shell, '''
+        Pattern p = Pattern.compile('(...)(...)')
+        Matcher m = p.matcher('foobar')
+        assert m.find()
+        assert m.group(1) == 'foo'
+        assert m.group(2) == 'bar'
+        '''
+        assertScript shell, '''
+        def m = 'barbaz' =~ /(...)(...)/
+        assert m[0][1] == 'bar'
+        assert m[0][2] == 'baz'
+        '''
+    }
+}


Mime
View raw message