Skip to content

Commit

Permalink
PT-13830: Support Java 21 string template syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
nrmancuso committed Dec 10, 2023
1 parent ee921b6 commit a38a23e
Show file tree
Hide file tree
Showing 8 changed files with 738 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public static String printBranch(DetailAST node) {
* @param ast the root AST node.
* @return string AST.
*/
private static String printTree(DetailAST ast) {
public static String printTree(DetailAST ast) {
final StringBuilder messageBuilder = new StringBuilder(1024);
DetailAST node = ast;
while (node != null) {
Expand Down
158 changes: 158 additions & 0 deletions src/main/java/com/puppycrawl/tools/checkstyle/JavaAstVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ public final class JavaAstVisitor extends JavaLanguageParserBaseVisitor<DetailAs
/** String representation of the right shift operator. */
private static final String RIGHT_SHIFT = ">>";

private static final String QUOTE = "\"";

private static final String EMBEDDED_EXPRESSION_BEGIN = "\\{";

private static final String EMBEDDED_EXPRESSION_END = "}";

/**
* The tokens here are technically expressions, but should
* not return an EXPR token as their root.
Expand Down Expand Up @@ -1533,6 +1539,14 @@ public DetailAstImpl visitPrimaryExp(JavaLanguageParser.PrimaryExpContext ctx) {
return flattenedTree(ctx);
}

@Override
public DetailAstImpl visitTemplateExp(JavaLanguageParser.TemplateExpContext ctx) {
final DetailAstImpl dot = create(ctx.DOT());
dot.addChild(visit(ctx.expr()));
dot.addChild(visit(ctx.templateArgument()));
return dot;
}

@Override
public DetailAstImpl visitPostfix(JavaLanguageParser.PostfixContext ctx) {
final DetailAstImpl postfix;
Expand Down Expand Up @@ -1742,6 +1756,150 @@ public DetailAstImpl visitPrimitivePrimary(JavaLanguageParser.PrimitivePrimaryCo
return dot;
}

@Override
public DetailAstImpl visitTemplateArgument(JavaLanguageParser.TemplateArgumentContext ctx) {
final DetailAstImpl templateArgument;
if (ctx.template() != null) {
templateArgument = visit(ctx.template());
}
else {
templateArgument = flattenedTree(ctx);
}
return templateArgument;
}

@Override
public DetailAstImpl visitStringTemplate(JavaLanguageParser.StringTemplateContext ctx) {
final DetailAstImpl begin = buildStringTemplateBeginning(ctx);

final DetailAstImpl embeddedExp = createImaginary(TokenTypes.EMBEDDED_EXPRESSION);
embeddedExp.addChild(visit(ctx.expr()));
begin.addChild(embeddedExp);

ctx.stringTemplateMiddle().stream()
.map(this::buildStringTemplateMiddle)
.collect(Collectors.toList())
.forEach(begin::addChild);

final DetailAstImpl end = buildStringTemplateEnd(ctx);
begin.addChild(end);
return begin;
}

private static DetailAstImpl buildStringTemplateBeginning(
JavaLanguageParser.StringTemplateContext ctx) {

final TerminalNode beginContext = ctx.STRING_TEMPLATE_BEGIN();
final Token beginToken = beginContext.getSymbol();

final String stringTemplateBeginText = beginContext.getText();
final int childCount = ctx.getChildCount();
final int charPositionInLine = beginToken.getCharPositionInLine();
final int beginTokenLine = beginToken.getLine();

final DetailAstImpl begin = buildImaginaryWithDetails(
TokenTypes.STRING_TEMPLATE_BEGIN, QUOTE,
beginTokenLine, charPositionInLine
);

final int start = QUOTE.length();
final int end = stringTemplateBeginText.length() - EMBEDDED_EXPRESSION_BEGIN.length();
final String beginContextText = beginContext.getText().substring(start, end);

final DetailAstImpl beginContent = buildImaginaryWithDetails(
TokenTypes.STRING_TEMPLATE_CONTENT, beginContextText,
beginTokenLine, charPositionInLine + start
);
begin.addChild(beginContent);

final DetailAstImpl embeddedBegin = buildImaginaryWithDetails(
TokenTypes.EMBEDDED_EXPRESSION_BEGIN, EMBEDDED_EXPRESSION_BEGIN,
beginTokenLine, charPositionInLine + end
);
begin.addChild(embeddedBegin);
return begin;
}

private static DetailAstImpl buildImaginaryWithDetails(
int tokenType, String text, int lineNumber, int columnNumber) {
final DetailAstImpl imaginary = createImaginary(tokenType);
imaginary.setText(text);
imaginary.setLineNo(lineNumber);
imaginary.setColumnNo(columnNumber);
return imaginary;
}

private DetailAstImpl buildStringTemplateMiddle(
JavaLanguageParser.StringTemplateMiddleContext middleContext) {
final TerminalNode ctx = middleContext.STRING_TEMPLATE_MID();
final Token token = ctx.getSymbol();
final int charPositionInLine = token.getCharPositionInLine();
final int lineNumber = token.getLine();
final String text = ctx.getText();

final DetailAstImpl embeddedEnd = buildImaginaryWithDetails(
TokenTypes.EMBEDDED_EXPRESSION_END, EMBEDDED_EXPRESSION_END,
lineNumber, charPositionInLine
);

final int startIndex = EMBEDDED_EXPRESSION_END.length();
final int endIndex = token.getText().length() - EMBEDDED_EXPRESSION_BEGIN.length();
final String contentText = text.substring(startIndex, endIndex);

final DetailAstImpl content = buildImaginaryWithDetails(
TokenTypes.STRING_TEMPLATE_CONTENT, contentText,
lineNumber, charPositionInLine + EMBEDDED_EXPRESSION_END.length()
);
embeddedEnd.addNextSibling(content);

final DetailAstImpl embeddedBegin = buildImaginaryWithDetails(
TokenTypes.EMBEDDED_EXPRESSION_BEGIN, EMBEDDED_EXPRESSION_BEGIN,
lineNumber,
charPositionInLine + text.length() - EMBEDDED_EXPRESSION_BEGIN.length()
);
content.addNextSibling(embeddedBegin);

final DetailAstImpl embeddedExp = createImaginary(TokenTypes.EMBEDDED_EXPRESSION);
embeddedExp.addChild(visit(middleContext.expr()));
embeddedBegin.addNextSibling(embeddedExp);
return embeddedEnd;
}

private static DetailAstImpl buildStringTemplateEnd(
JavaLanguageParser.StringTemplateContext ctx) {

final TerminalNode endContext = ctx.STRING_TEMPLATE_END();
final Token endToken = endContext.getSymbol();

final String stringTemplateEndText = endContext.getText();
final int childCount = ctx.getChildCount();
final int charPositionInLine = endToken.getCharPositionInLine();
final int lineNumber = endToken.getLine();

final int startIndex = EMBEDDED_EXPRESSION_END.length();
final int endIndex = stringTemplateEndText.length() - QUOTE.length();
final String endContentContentText = stringTemplateEndText.substring(startIndex, endIndex);

final DetailAstImpl embeddedEnd = buildImaginaryWithDetails(
TokenTypes.EMBEDDED_EXPRESSION_END, EMBEDDED_EXPRESSION_END,
lineNumber, charPositionInLine
);

final DetailAstImpl endContent = buildImaginaryWithDetails(
TokenTypes.STRING_TEMPLATE_CONTENT, endContentContentText,
lineNumber, charPositionInLine + QUOTE.length()
);
embeddedEnd.addNextSibling(endContent);

final DetailAstImpl end = buildImaginaryWithDetails(
TokenTypes.STRING_TEMPLATE_END, QUOTE,
lineNumber,
charPositionInLine + stringTemplateEndText.length() - QUOTE.length()
);
endContent.addNextSibling(end);
return embeddedEnd;
}

@Override
public DetailAstImpl visitCreator(JavaLanguageParser.CreatorContext ctx) {
return flattenedTree(ctx);
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/puppycrawl/tools/checkstyle/api/TokenTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -6469,6 +6469,21 @@ public final class TokenTypes {
public static final int RECORD_PATTERN_COMPONENTS =
JavaLanguageLexer.RECORD_PATTERN_COMPONENTS;

public static final int STRING_TEMPLATE_BEGIN =
JavaLanguageLexer.STRING_TEMPLATE_BEGIN;
public static final int STRING_TEMPLATE_MID =
JavaLanguageLexer.STRING_TEMPLATE_MID;
public static final int STRING_TEMPLATE_END =
JavaLanguageLexer.STRING_TEMPLATE_END;
public static final int STRING_TEMPLATE_CONTENT =
JavaLanguageLexer.STRING_TEMPLATE_CONTENT;
public static final int EMBEDDED_EXPRESSION_BEGIN =
JavaLanguageLexer.EMBEDDED_EXPRESSION_BEGIN;
public static final int EMBEDDED_EXPRESSION =
JavaLanguageLexer.EMBEDDED_EXPRESSION;
public static final int EMBEDDED_EXPRESSION_END =
JavaLanguageLexer.EMBEDDED_EXPRESSION_END;

/** Prevent instantiation. */
private TokenTypes() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ tokens {
LITERAL_NON_SEALED, LITERAL_SEALED, LITERAL_PERMITS,
PERMITS_CLAUSE, PATTERN_DEF, LITERAL_WHEN,
RECORD_PATTERN_DEF, RECORD_PATTERN_COMPONENTS
RECORD_PATTERN_DEF, RECORD_PATTERN_COMPONENTS,
STRING_TEMPLATE_BEGIN, STRING_TEMPLATE_MID, STRING_TEMPLATE_END,
STRING_TEMPLATE_CONTENT, EMBEDDED_EXPRESSION_BEGIN, EMBEDDED_EXPRESSION,
EMBEDDED_EXPRESSION_END
}

@header {
Expand Down Expand Up @@ -148,6 +152,10 @@ import com.puppycrawl.tools.checkstyle.grammar.CrAwareLexerSimulator;

/** Tracks the starting column of a block comment. */
int startCol = -1;

private boolean isTextBlockMode() {
return _modeStack.size() > 0 && _modeStack.peek() == TextBlock;
}
}

// Keywords and restricted identifiers
Expand Down Expand Up @@ -242,7 +250,14 @@ LITERAL_FALSE: 'false';

CHAR_LITERAL: '\'' (EscapeSequence | ~['\\\r\n]) '\'';

STRING_LITERAL: '"' (EscapeSequence | ~["\\\r\n])* '"';
fragment StringFragment: (EscapeSequence | ~["\\\r\n])*;
STRING_LITERAL: '"' StringFragment '"';
// explore if we can gather StringFragment and emit a token
STRING_TEMPLATE_BEGIN: '"' StringFragment '\\' '{';
STRING_TEMPLATE_MID: '}' StringFragment '\\' '{';
STRING_TEMPLATE_END: '}' StringFragment '"';
TEXT_BLOCK_LITERAL_BEGIN: '"' '"' '"' -> pushMode(TextBlock);
Expand Down Expand Up @@ -403,7 +418,20 @@ fragment Letter
// Text block lexical mode
mode TextBlock;
TEXT_BLOCK_CONTENT
: ( TwoDoubleQuotes
: TextBlockFragment
;
TEXT_BLOCK_TEMPLATE_BEGIN
: TextBlockFragment '\\' '{' -> pushMode(DEFAULT_MODE);
TEXT_BLOCK_TEMPLATE_MID
: '}' TextBlockFragment '\\' '{';
TEXT_BLOCK_TEMPLATE_END
: '}' TextBlockFragment TEXT_BLOCK_LITERAL_END -> popMode;
fragment TextBlockFragment
: ( TwoDoubleQuotes
| OneDoubleQuote
| Newline
| ~'"'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,7 @@ expression

expr
: primary #primaryExp
| expr DOT templateArgument #templateExp
| expr bop=DOT id #refOp
| expr bop=DOT id LPAREN expressionList? RPAREN #methodCall
| expr bop=DOT LITERAL_THIS #thisExp
Expand Down Expand Up @@ -757,6 +758,24 @@ primary
DOT LITERAL_CLASS #primitivePrimary
;

templateArgument
: template
| STRING_LITERAL
| TEXT_BLOCK_LITERAL_BEGIN TEXT_BLOCK_CONTENT TEXT_BLOCK_LITERAL_END
;

template
: stringTemplate
;

stringTemplate
: STRING_TEMPLATE_BEGIN expr? stringTemplateMiddle* STRING_TEMPLATE_END
;

stringTemplateMiddle
: STRING_TEMPLATE_MID expr?
;

classType
: (classOrInterfaceType[false] DOT)? annotations[false] id typeArguments?
;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2023 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
///////////////////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.grammar.java21;

import org.junit.jupiter.api.Test;

import com.puppycrawl.tools.checkstyle.AbstractTreeTestSupport;

public class Java21AstRegressionTest extends AbstractTreeTestSupport {

@Override
protected String getPackageLocation() {
return "com/puppycrawl/tools/checkstyle/grammar/java21";
}

@Test
public void testBasicStringTemplate() throws Exception {
verifyAst(
getNonCompilablePath(
"ExpectedStringTemplateBasic.txt"),
getNonCompilablePath(
"InputStringTemplateBasic.java"));
}
}
Loading

0 comments on commit a38a23e

Please sign in to comment.