[
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)