Copilot commented on code in PR #15397:
URL: https://github.com/apache/grails-core/pull/15397#discussion_r2816941209


##########
grails-console/src/main/groovy/grails/ui/command/GrailsApplicationContextCommandRunner.groovy:
##########
@@ -85,6 +97,32 @@ class GrailsApplicationContextCommandRunner extends 
DevelopmentGrailsApplication
         return null
     }
 
+    /**
+     * Filters out command-specific options (arguments starting with '--') 
from the
+     * args array before they are passed to Spring Boot's {@code 
SpringApplication.run()}.
+     *
+     * <p>Spring Boot interprets {@code --key=value} arguments as property 
overrides via
+     * {@code CommandLinePropertySource}. When Grails command options like
+     * {@code --dataSource=analytics} are passed through, Spring Boot sets
+     * {@code dataSource=analytics} as a top-level property, corrupting GORM's 
datasource
+     * configuration which expects {@code dataSource} to be a Map containing 
url, username, etc.</p>
+     *
+     * <p>Command options are still available to the Grails command via
+     * {@code CommandLineParser.parse(args)} which receives the unfiltered 
args.</p>
+     *
+     * @param args the full argument array including command options
+     * @return a filtered array with command options removed, safe for Spring 
Boot
+     */
+    static String[] filterCommandOptions(String[] args) {
+        List<String> filtered = new ArrayList<>()
+        for (String arg : args) {
+            if (!arg.startsWith('--')) {
+                filtered.add(arg)
+            }
+        }
+        filtered.toArray(new String[0])
+    }

Review Comment:
   The filter doesn't handle the edge case of arguments that are exactly "--" 
(with no key or value). While rare, if such an argument exists, it would be 
filtered out. Consider whether this is the intended behavior, as "--" is 
commonly used as a delimiter in command-line parsing to separate options from 
positional arguments. If this is intentional, it should be documented.



##########
grails-console/src/main/groovy/grails/ui/command/GrailsApplicationContextCommandRunner.groovy:
##########
@@ -85,6 +97,32 @@ class GrailsApplicationContextCommandRunner extends 
DevelopmentGrailsApplication
         return null
     }
 
+    /**
+     * Filters out command-specific options (arguments starting with '--') 
from the
+     * args array before they are passed to Spring Boot's {@code 
SpringApplication.run()}.
+     *
+     * <p>Spring Boot interprets {@code --key=value} arguments as property 
overrides via
+     * {@code CommandLinePropertySource}. When Grails command options like
+     * {@code --dataSource=analytics} are passed through, Spring Boot sets
+     * {@code dataSource=analytics} as a top-level property, corrupting GORM's 
datasource
+     * configuration which expects {@code dataSource} to be a Map containing 
url, username, etc.</p>
+     *
+     * <p>Command options are still available to the Grails command via
+     * {@code CommandLineParser.parse(args)} which receives the unfiltered 
args.</p>
+     *
+     * @param args the full argument array including command options
+     * @return a filtered array with command options removed, safe for Spring 
Boot
+     */
+    static String[] filterCommandOptions(String[] args) {
+        List<String> filtered = new ArrayList<>()
+        for (String arg : args) {
+            if (!arg.startsWith('--')) {
+                filtered.add(arg)
+            }
+        }
+        filtered.toArray(new String[0])
+    }

Review Comment:
   The filterCommandOptions method doesn't handle null elements within the args 
array. If any element in the array is null, calling startsWith('--') will throw 
a NullPointerException. Consider adding a null check before accessing the 
argument.



##########
grails-console/src/test/groovy/grails/ui/command/GrailsApplicationContextCommandRunnerSpec.groovy:
##########
@@ -0,0 +1,124 @@
+/*
+ *  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
+ *
+ *      https://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 grails.ui.command
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+/**
+ * Tests for {@link 
GrailsApplicationContextCommandRunner#filterCommandOptions(String[])}.
+ *
+ * Verifies that command-specific options (--key=value, --flag) are filtered 
out
+ * before being passed to Spring Boot's SpringApplication.run(), while 
preserving
+ * non-option arguments like the command name.
+ */
+class GrailsApplicationContextCommandRunnerSpec extends Specification {
+
+    @Unroll
+    def "filterCommandOptions filters '#description'"() {
+        expect:
+        GrailsApplicationContextCommandRunner.filterCommandOptions(input as 
String[]) == expected as String[]
+
+        where:
+        description                              | input                       
                             | expected
+        'single --dataSource option'             | ['dbm-status', 
'--dataSource=analytics']                 | ['dbm-status']
+        'multiple command options'               | ['dbm-status', 
'--dataSource=analytics', '--contexts=dev'] | ['dbm-status']
+        'no options (passthrough)'               | ['dbm-status']              
                             | ['dbm-status']
+        'empty args'                             | []                          
                             | []
+        'only options (no command)'              | ['--dataSource=analytics']  
                             | []
+        'mixed options and positional args'      | ['dbm-gorm-diff', 
'--dataSource=analytics', 'output.groovy'] | ['dbm-gorm-diff', 'output.groovy']
+        'flag without value'                     | ['dbm-update', '--verbose'] 
                             | ['dbm-update']
+        'single dash args preserved'             | ['dbm-status', '-v', 
'--dataSource=analytics']           | ['dbm-status', '-v']
+        // Database Migration Plugin: --contexts and --defaultSchema
+        'database migration --contexts option'   | ['dbm-update', 
'--contexts=production']                  | ['dbm-update']
+        'database migration --defaultSchema'     | ['dbm-update', 
'--defaultSchema=myschema']               | ['dbm-update']
+        'all three dbm options together'         | ['dbm-gorm-diff', 
'--dataSource=analytics', '--contexts=dev', '--defaultSchema=public'] | 
['dbm-gorm-diff']
+        'dbm command with positional + options'  | ['dbm-gorm-diff', 
'output.groovy', '--dataSource=analytics', '--add'] | ['dbm-gorm-diff', 
'output.groovy']
+        // Spring Security Plugin: --groupClassName
+        'spring security --groupClassName'       | ['s2-quickstart', 
'com.example', 'User', 'Role', '--groupClassName=RoleGroup'] | 
['s2-quickstart', 'com.example', 'User', 'Role']
+        // Script runner: --option in script args
+        'script runner with --option'            | ['myscript.groovy', 
'--someOption=value']                | ['myscript.groovy']
+    }
+
+    def "filterCommandOptions preserves argument order"() {
+        given:
+        String[] input = ['first', 'second', '--removed', 'third'] as String[]
+
+        when:
+        String[] result = 
GrailsApplicationContextCommandRunner.filterCommandOptions(input)
+
+        then:
+        result == ['first', 'second', 'third'] as String[]
+    }
+
+    def "filterCommandOptions returns new array instance"() {
+        given:
+        String[] input = ['dbm-status'] as String[]
+
+        when:
+        String[] result = 
GrailsApplicationContextCommandRunner.filterCommandOptions(input)
+
+        then:
+        !result.is(input)
+        result == input
+    }
+
+    def "filterCommandOptions handles typical Gradle dbm task args"() {
+        given: 'args as they arrive from ApplicationContextCommandTask via 
GrailsGradlePlugin'
+        // configureApplicationCommands() passes: [commandName, ...userArgs, 
applicationClassName]
+        // main() extracts: commandName = args[0], appClass = args.last()
+        // run() receives: args.init() = [commandName, ...userArgs]
+        String[] runArgs = ['dbm-status', '--dataSource=analytics'] as String[]
+
+        when:
+        String[] springBootArgs = 
GrailsApplicationContextCommandRunner.filterCommandOptions(runArgs)
+
+        then: 'only the command name reaches Spring Boot'
+        springBootArgs == ['dbm-status'] as String[]
+        springBootArgs.length == 1
+    }
+
+    def "filterCommandOptions prevents dataSource property corruption"() {
+        given: 'the problematic args that caused the original bug'
+        String[] args = ['dbm-gorm-diff', '--dataSource=analytics', 
'--contexts=production'] as String[]
+
+        when:
+        String[] filtered = 
GrailsApplicationContextCommandRunner.filterCommandOptions(args)
+
+        then: 'no --key=value args remain to corrupt Spring Boot properties'
+        filtered.every { !it.startsWith('--') }
+        filtered == ['dbm-gorm-diff'] as String[]
+    }
+
+    def "filterCommandOptions protects all known Grails ecosystem command 
options"() {
+        expect: 'every known --option from Grails plugins is filtered'
+        GrailsApplicationContextCommandRunner.filterCommandOptions(input as 
String[]) == expected as String[]
+
+        where:
+        input                                                                | 
expected
+        // Database Migration Plugin (30+ commands accept these)
+        ['dbm-status', '--dataSource=analytics']                             | 
['dbm-status']
+        ['dbm-update', '--contexts=production,staging']                      | 
['dbm-update']
+        ['dbm-gorm-diff', '--defaultSchema=public']                          | 
['dbm-gorm-diff']
+        ['dbm-status', '--verbose']                                          | 
['dbm-status']
+        ['dbm-diff', '--add']                                                | 
['dbm-diff']
+        // Spring Security Plugin (s2-quickstart)
+        ['s2-quickstart', 'com.example', 'User', 'Role', 
'--groupClassName=RoleGroup'] | ['s2-quickstart', 'com.example', 'User', 'Role']
+        // Custom ApplicationCommand with arbitrary --options
+        ['my-command', '--customFlag', '--anotherOption=value']              | 
['my-command']
+    }

Review Comment:
   Test coverage is missing for the edge case where args array contains null 
elements. This should be tested to ensure the implementation handles nulls 
gracefully (either by skipping them or throwing an appropriate error).
   ```suggestion
       }
   
       def "filterCommandOptions handles null elements in args"() {
           given:
           String[] input = ['dbm-status', null, '--dataSource=analytics'] as 
String[]
   
           when:
           GrailsApplicationContextCommandRunner.filterCommandOptions(input)
   
           then:
           noExceptionThrown()
       }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to