diff --git a/build-conventions/src/main/java/org/elasticsearch/gradle/internal/checkstyle/StringFormattingCheck.java b/build-conventions/src/main/java/org/elasticsearch/gradle/internal/checkstyle/StringFormattingCheck.java new file mode 100644 index 0000000000000..48fa3ad6ee485 --- /dev/null +++ b/build-conventions/src/main/java/org/elasticsearch/gradle/internal/checkstyle/StringFormattingCheck.java @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.checkstyle; + +import com.puppycrawl.tools.checkstyle.StatelessCheck; +import com.puppycrawl.tools.checkstyle.api.AbstractCheck; +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.api.TokenTypes; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Checks for calls to {@link String#formatted(Object...)} that include format specifiers that + * are not locale-safe. This method always uses the default {@link Locale}, and so for our + * purposes it is safer to use {@link String#format(Locale, String, Object...)}. + *

+ * Note that this rule can currently only detect violations when calling formatted() + * on a string literal or text block. In theory, it could be extended to detect violations in + * local variables or statics. + */ +@StatelessCheck +public class StringFormattingCheck extends AbstractCheck { + + public static final String FORMATTED_MSG_KEY = "forbidden.formatted"; + + @Override + public int[] getDefaultTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getAcceptableTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getRequiredTokens() { + return new int[] { TokenTypes.METHOD_CALL }; + } + + @Override + public void visitToken(DetailAST ast) { + checkFormattedMethod(ast); + } + + // Originally pinched from java/util/Formatter.java but then modified. + // %[argument_index$][flags][width][.precision][t]conversion + private static final Pattern formatSpecifier = Pattern.compile("%(?:\\d+\\$)?(?:[-#+ 0,\\(<]*)?(?:\\d+)?(?:\\.\\d+)?([tT]?[a-zA-Z%])"); + + private void checkFormattedMethod(DetailAST ast) { + final DetailAST dotAst = ast.findFirstToken(TokenTypes.DOT); + if (dotAst == null) { + return; + } + + final String methodName = dotAst.findFirstToken(TokenTypes.IDENT).getText(); + if (methodName.equals("formatted") == false) { + return; + } + + final DetailAST subjectAst = dotAst.getFirstChild(); + + String stringContent = null; + if (subjectAst.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN) { + stringContent = subjectAst.findFirstToken(TokenTypes.TEXT_BLOCK_CONTENT).getText(); + } else if (subjectAst.getType() == TokenTypes.STRING_LITERAL) { + stringContent = subjectAst.getText(); + } + + if (stringContent != null) { + final Matcher m = formatSpecifier.matcher(stringContent); + while (m.find()) { + char specifier = m.group(1).toLowerCase(Locale.ROOT).charAt(0); + + if (specifier == 'd' || specifier == 'e' || specifier == 'f' || specifier == 'g' || specifier == 't') { + log(ast, FORMATTED_MSG_KEY, m.group()); + } + } + } + } +} diff --git a/build-tools-internal/src/main/resources/checkstyle.xml b/build-tools-internal/src/main/resources/checkstyle.xml index 95a32e026c1ff..22a0d2912e4d7 100644 --- a/build-tools-internal/src/main/resources/checkstyle.xml +++ b/build-tools-internal/src/main/resources/checkstyle.xml @@ -130,6 +130,12 @@ --> + + + +