[ 
https://issues.apache.org/jira/browse/GROOVY-12696?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King resolved GROOVY-12696.
--------------------------------
    Fix Version/s: 7.0.0-alpha-1
                   6.0.1
       Resolution: Fixed

I added also one more case as a secondary commit.

> FormatStringChecker treats explicit argument index as zero-based when 
> arguments are not constants
> -------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12696
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12696
>             Project: Groovy
>          Issue Type: Bug
>    Affects Versions: 6.0.0
>            Reporter: Scott Murphy Heiberg
>            Assignee: Paul King
>            Priority: Major
>             Fix For: 7.0.0-alpha-1, 6.0.1
>
>
> *Environment:* macOS 26.5, JDK 21.0.12 (BellSoft Liberica), Groovy 6.0.0, 
> 6.0.1-SNAPSHOT (GROOVY_6_0_X at 7c0dcf17dd) and master (7.0.0-SNAPSHOT at 
> d86ea7eb85)
> {{groovy.typecheckers.FormatStringChecker}} has two paths. When every 
> argument of a format call is a constant it runs {{java.util.Formatter}} 
> itself and reports whatever {{Formatter}} throws. When at least one argument 
> is not a constant it walks the format specifiers and checks each argument's 
> static type. On that second path an explicit argument index ({{%1$s}}, 
> {{%2$d}}, ...) is used directly as a zero-based position in the argument 
> list, while {{Formatter}} numbers explicit indices from 1. The checker 
> therefore looks at the argument after the one {{Formatter}} will use: it 
> rejects valid calls and accepts calls that throw at run time.
> h3. Reproducer
> {code:groovy}
> import groovy.transform.TypeChecked
> @TypeChecked(extensions = 'groovy.typecheckers.FormatStringChecker')
> class Repro {
>     static String run(String s, Integer i) {
>         String.format('%1$s', s)
>     }
> }
> println '[' + Repro.run('x', 1) + ']'
> {code}
> Actual output ({{groovy Repro.groovy}}):
> {noformat}
> org.codehaus.groovy.control.MultipleCompilationErrorsException: startup 
> failed:
> Repro.groovy: 6: [Static type checking] - MissingFormatArgument: Format 
> specifier '%1$s'
>  @ line 6, column 9.
>            String.format('%1$s', s)
>            ^
> 1 error
> {noformat}
> Expected: the script compiles and prints {{[x]}}. {{String.format('%1$s', 
> 'x')}} is valid and returns {{x}}; with the constant argument {{'x'}} in 
> place of {{s}} the checker accepts the call.
> The opposite case, with the same class but
> {code:groovy}
>         String.format('%1$d', s, i)
> {code}
> compiles without a type error and then fails at run time:
> {noformat}
> Caught: java.util.IllegalFormatConversionException: d != java.lang.String
> {noformat}
> Expected: a compile-time error {{IllegalFormatConversion: d != 
> java.lang.String}}, which is what the checker reports for the same call with 
> constant arguments, {{String.format('%1$d', 'x', 1)}}.
> More calls with {{String s}} and {{Integer i}} parameters, run on all three 
> versions listed above:
> || call || Formatter at run time || FormatStringChecker ||
> | {{String.format('%1$s', s)}} | {{x}} | MissingFormatArgument: Format 
> specifier '%1$s' |
> | {{String.format('%2$s', s, i)}} | {{1}} | MissingFormatArgument: Format 
> specifier '%2$s' |
> | {{String.format('%2$d %1$s', s, i)}} | {{1 x}} | MissingFormatArgument: 
> Format specifier '%2$d' |
> | {{String.format('%1$s %2$d', s, i)}} | {{x 1}} | MissingFormatArgument: 
> Format specifier '%2$d' |
> | {{String.format('%2$s %s %<s %1$s', s, i)}} | {{1 x x x}} | 
> MissingFormatArgument: Format specifier '%2$s' |
> | {{String.format('%1$d', i, s)}} | {{1}} | IllegalFormatConversion: d != 
> java.lang.String (checks {{s}} instead of {{i}}) |
> | {{String.format('%1$d', s, i)}} | IllegalFormatConversionException: d != 
> java.lang.String | accepted |
> | {{String.format('%3$s', s, i)}} | MissingFormatArgumentException | 
> MissingFormatArgument: Format specifier '%3$s' (correct only because 3 and 2 
> both exceed the last index) |
> | {{String.format('%1$s', 'x')}} (constants) | {{x}} | accepted |
> | {{String.format('%1$d', 'x', 1)}} (constants) | 
> IllegalFormatConversionException | IllegalFormatConversion: d != 
> java.lang.String |
> h3. Cause
> In {{FormatStringChecker.checkFormatStringTypes}} 
> (subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy,
>  line 184 on master):
> {code:groovy}
> int indexToUse = argIndex ?: next
> {code}
> {{argIndex}} is the integer parsed from the {{n$}} prefix and is used as is 
> to index {{args}}, which is zero-based. The ordinary counter {{next}} and the 
> index remembered for {{%<}} are zero-based, so an explicit index is off by 
> one relative to them and to {{Formatter}}, whose {{format}} loop uses {{index 
> - 1}} for an explicit index. The bounds check {{indexToUse >= args.size()}} 
> then rejects {{%n$}} whenever {{n}} equals the argument count, and the type 
> check otherwise inspects argument {{n + 1}} instead of {{n}}. The 
> constant-argument path ({{checkFormatStringConstantArgs}}) is not affected.
> The fix is to subtract one from an explicit index before using it: {{int 
> indexToUse = argIndex ? argIndex - 1 : next}}. With that change every call in 
> the table above is checked the same way {{Formatter}} resolves it. A pull 
> request with regression tests follows.
> h3. Versions
> Reproduces on Groovy 6.0.0, 6.0.1-SNAPSHOT (GROOVY_6_0_X at 7c0dcf17dd) and 
> master (d86ea7eb85). The line has been unchanged since the checker was added 
> for GROOVY-11261 (5.0.0-alpha-5), so 5.0.x is presumably affected as well, 
> though that was not run.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to