GROOVY-11261: Provide a custom type checker for format strings (additional tests)
diff --git a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy
index f7ecb10..0e08631 100644
--- a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy
+++ b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy
@@ -147,9 +147,28 @@
void checkFormatStringTypes(Expression expression, List<Expression> args, Expression target) {
int next = 0
+ int prevIndex = -1
expression.value.eachMatch(formatSpecifier) { spec ->
def (all, argIndex, flags, width, precision, tgroup, conversion) = spec
- def arg = args[argIndex ?: next]
+ def flagList = flags?.toList()
+ if (argIndex) {
+ argIndex -= '$'
+ argIndex = argIndex.toInteger()
+ }
+ int indexToUse = argIndex ?: next
+ if (flagList.contains('<')) {
+ if (prevIndex == -1) {
+ addStaticTypeError("MissingFormatArgument: Format specifier '$all'", target)
+ return
+ } else {
+ indexToUse = prevIndex
+ }
+ }
+ if (indexToUse >= args.size()) {
+ addStaticTypeError("MissingFormatArgument: Format specifier '$all'", target)
+ return
+ }
+ def arg = args[indexToUse]
Object type = getWrapper(getType(arg)).typeClass
if (tgroup) {
if (!'HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc'.contains(conversion)) {
@@ -161,13 +180,11 @@
if (!([Long, Calendar, Date, TemporalAccessor].any { it.isAssignableFrom(type) })) {
addStaticTypeError("IllegalFormatConversion: $conversion != $type.name", target)
}
- def flagList = flags?.toList()
checkBadFlags(flagList, conversion, target, '#+ 0,(')
if (flagList.contains('-') && !width) {
addStaticTypeError("MissingFormatWidth: $all", target)
}
} else {
- def flagList = flags?.toList()
def dupFlag = flagList.countBy().find { flag, count -> count > 1 }?.key
if (dupFlag) {
addStaticTypeError("DuplicateFormatFlags: Flags = '$dupFlag'", target)
@@ -224,12 +241,10 @@
addStaticTypeError("UnknownFormatConversion: Conversion = '${conversion}'", target)
}
}
- if ((argIndex ?: next) > args.size()) {
- addStaticTypeError("Bad index", target)
- } else {
- println 'Inferred type: ' + type
+ prevIndex = indexToUse
+ if (!argIndex && !flagList.contains('<')) {
+ next++
}
- next++ // TODO look for <
}
}
diff --git a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
index eef34b2..a43e7be 100644
--- a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
+++ b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
@@ -40,7 +40,7 @@
simplify application of such checkers, e.g. make it apply globally
using a compiler configuration script, as just one example.
-== Checking Regex literals
+== Checking Regular Expressions
Consider the following code, which contains 3 regular expressions:
@@ -109,7 +109,7 @@
These methods have the following characteristics:
-* The first argument must be of type api:java.util.Locale[] or api:java.lang.String[].
+* The first argument must be of type `java.util.Locale` or `java.lang.String`.
* If the first argument is of type `Locale`, the second argument must be of type `String`.
* The `String` argument is treated as a format string containing zero or more embedded format specifiers as well as plain text. The format specifiers determine how the remaining arguments will be used within the resulting output.
* The `FormatStringChecker` ensures that the format string is valid and the remaining arguments are compatible with the embedded format specifiers.
@@ -152,7 +152,7 @@
----
|===
-Here is an example of correctly using some of these methods:
+Here is an example of correctly using some of these methods with type checking in place:
[source,groovy]
----
@@ -170,6 +170,13 @@
You can use the `FormatMethod` annotation provided in the `groovy-typecheckers`
module or the https://checkerframework.org/api/org/checkerframework/checker/formatter/qual/FormatMethod.html[similarly named] annotation from the Java-based https://checkerframework.org/[checker framework].
+The format string checker looks for a constant literal
+format string. For arguments, it also looks for constant
+literals or otherwise makes checks based on inferred type.
+When looking for constants, the checker will find inline
+literals, local variables with a constant initializer,
+and fields with a constant initializer.
+
=== Errors detected
Here are some examples of the kinds of errors detected.
@@ -226,3 +233,4 @@
* unknown format conversions
* illegal format conversions
* format flags conversion mismatches
+* missing arguments
diff --git a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/FormatStringCheckerTest.groovy b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/FormatStringCheckerTest.groovy
index f0a83f8..51f9d09 100644
--- a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/FormatStringCheckerTest.groovy
+++ b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/FormatStringCheckerTest.groovy
@@ -24,7 +24,9 @@
import org.junit.Test
import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.isAtLeastJdk
import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.Assume.assumeTrue
final class FormatStringCheckerTest {
@@ -256,6 +258,38 @@
}
@Test
+ void testMissingFormatArgumentForConstantArgs() {
+ def err = shouldFail shell, '''
+ String.format('%<s', 'some string')
+ '''
+ assert err =~ /MissingFormatArgument: Format specifier '%<s'/
+ err = shouldFail shell, '''
+ String.format('%s %s', 'some string')
+ '''
+ assert err =~ "MissingFormatArgument: Format specifier '%s'"
+ err = shouldFail shell, '''
+ String.format('%2$s', 'some string')
+ '''
+ assert err =~ /MissingFormatArgument: Format specifier '%\d[$]s'/
+ }
+
+ @Test
+ void testMissingFormatArgumentForInferredArgs() {
+ def err = shouldFail shell, '''
+ String.format('%<s', 'some string' + '')
+ '''
+ assert err =~ /MissingFormatArgument: Format specifier '%<s'/
+ err = shouldFail shell, '''
+ String.format('%s %s', 'some string' + '')
+ '''
+ assert err =~ "MissingFormatArgument: Format specifier '%s'"
+ err = shouldFail shell, '''
+ String.format('%2$s', 'some string' + '')
+ '''
+ assert err =~ /MissingFormatArgument: Format specifier '%\d[$]s'/
+ }
+
+ @Test
void testValidFormatStringsConstantArgs() {
assertScript shell, '''
static int x = 254
@@ -275,8 +309,20 @@
assert sprintf('%3s', 'abcde') == 'abcde'
assert sprintf('%6s', 'abcde') == ' abcde'
assert sprintf('%-6s', 'abcde') == 'abcde '
+ assert String.format('%2$s %1$s', 'bar', 'foo') == 'foo bar'
+ assert String.format('%1$s %1$s', 'bar', 'foo') == 'bar bar'
+ assert String.format('%s %<s', 'bar', 'foo') == 'bar bar'
+ assert String.format('%2$s %<s', 'bar', 'foo') == 'foo foo'
+ assert String.format('%2$s %2$s', 'bar', 'foo') == 'foo foo'
}
'''
}
+ @Test
+ void testValidFormattedCall() {
+ assumeTrue(isAtLeastJdk('15.0'))
+ assertScript shell, '''
+ assert '%x'.formatted(16) == '10'
+ '''
+ }
}