diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParser.java b/java-frontend/src/main/java/org/sonar/java/model/JParser.java index 8ca41dd336c..0fb06ac7e3b 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParser.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParser.java @@ -575,20 +575,32 @@ private JavaTree.CompilationUnitTreeImpl convertCompilationUnit(CompilationUnit for (int i = 0; i < e.imports().size(); i++) { ImportDeclaration e2 = (ImportDeclaration) e.imports().get(i); ExpressionTree name = convertImportName(e2.getName()); - if (e2.isOnDemand()) { - name = new MemberSelectExpressionTreeImpl( - name, - lastTokenIn(e2, TerminalToken.TokenNameDOT), - new IdentifierTreeImpl(lastTokenIn(e2, TerminalToken.TokenNameMULTIPLY)) - ); + + boolean isModuleImport = org.eclipse.jdt.core.dom.Modifier.isModule(e2.getModifiers()); + + // "on demand" means `import pkg.*;` + if (e2.isOnDemand() && !isModuleImport) { + InternalSyntaxToken dotToken = lastTokenIn(e2, TerminalToken.TokenNameDOT); + InternalSyntaxToken identifierToken = lastTokenIn(e2, TerminalToken.TokenNameMULTIPLY); + name = new MemberSelectExpressionTreeImpl(name, dotToken, new IdentifierTreeImpl(identifierToken)); } + + InternalSyntaxToken staticKeyword = e2.isStatic() ? firstTokenIn(e2, TerminalToken.TokenNamestatic) : null; + InternalSyntaxToken moduleKeyword = isModuleImport ? firstTokenIn(e2, TerminalToken.TokenNamemodule) : null; + JavaTree.ImportTreeImpl t = new JavaTree.ImportTreeImpl( firstTokenIn(e2, TerminalToken.TokenNameimport), - e2.isStatic() ? firstTokenIn(e2, TerminalToken.TokenNamestatic) : null, + staticKeyword, + moduleKeyword, name, lastTokenIn(e2, TerminalToken.TokenNameSEMICOLON) ); - t.binding = e2.resolveBinding(); + + if (!isModuleImport) { + // There is no method to resolve bindings for a module import. + t.binding = e2.resolveBinding(); + } + imports.add(t); int tokenIndex = tokenManager.lastIndexIn(e2, TerminalToken.TokenNameSEMICOLON); diff --git a/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java b/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java index d98b9236b22..29681c5d8cf 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java @@ -317,20 +317,27 @@ public static String packageNameAsString(@Nullable PackageDeclarationTree tree) public static class ImportTreeImpl extends JavaTree implements ImportTree { private final boolean isStatic; + private final boolean isModule; private final Tree qualifiedIdentifier; private final SyntaxToken semicolonToken; private final SyntaxToken importToken; + @Nullable private final SyntaxToken staticToken; + @Nullable + private final SyntaxToken moduleToken; public IBinding binding; public ImportTreeImpl(InternalSyntaxToken importToken, @Nullable InternalSyntaxToken staticToken, + @Nullable InternalSyntaxToken moduleToken, Tree qualifiedIdentifier, InternalSyntaxToken semiColonToken) { this.importToken = importToken; this.staticToken = staticToken; + this.moduleToken = moduleToken; this.qualifiedIdentifier = qualifiedIdentifier; this.semicolonToken = semiColonToken; isStatic = staticToken != null; + isModule = moduleToken != null; } @Nullable @@ -358,6 +365,11 @@ public boolean isStatic() { return isStatic; } + @Override + public boolean isModule() { + return isModule; + } + @Override public SyntaxToken importKeyword() { return importToken; @@ -369,6 +381,12 @@ public SyntaxToken staticKeyword() { return staticToken; } + @Nullable + @Override + public SyntaxToken moduleKeyword() { + return moduleToken; + } + @Override public Tree qualifiedIdentifier() { return qualifiedIdentifier; @@ -389,6 +407,7 @@ public List children() { return ListUtils.concat( Collections.singletonList(importToken), isStatic ? Collections.singletonList(staticToken) : Collections.emptyList(), + isModule ? Collections.singletonList(moduleToken) : Collections.emptyList(), Arrays.asList(qualifiedIdentifier, semicolonToken)); } } diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/tree/ImportTree.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/tree/ImportTree.java index 02bd1d2bb27..5fc45b7abf9 100644 --- a/java-frontend/src/main/java/org/sonar/plugins/java/api/tree/ImportTree.java +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/tree/ImportTree.java @@ -41,11 +41,22 @@ public interface ImportTree extends ImportClauseTree { */ boolean isStatic(); + /** + * @since Java 25 + */ + boolean isModule(); + SyntaxToken importKeyword(); @Nullable SyntaxToken staticKeyword(); + @Nullable + /** + * @since Java 25 + */ + SyntaxToken moduleKeyword(); + Tree qualifiedIdentifier(); SyntaxToken semicolonToken(); diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java index bd7b0a17661..db1d07bcef0 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java @@ -47,6 +47,7 @@ import org.sonar.java.model.expression.MethodInvocationTreeImpl; import org.sonar.java.model.expression.MethodReferenceTreeImpl; import org.sonar.java.model.expression.NewClassTreeImpl; +import org.sonar.java.model.location.InternalRange; import org.sonar.java.model.statement.BlockTreeImpl; import org.sonar.java.model.statement.BreakStatementTreeImpl; import org.sonar.java.model.statement.ContinueStatementTreeImpl; @@ -58,6 +59,7 @@ import org.sonar.java.model.statement.SwitchExpressionTreeImpl; import org.sonar.java.model.statement.YieldStatementTreeImpl; import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.location.Position; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Symbol.MethodSymbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; @@ -2066,4 +2068,38 @@ void f2() { assertNotEquals(pOfB.symbol(), pOfC.symbol()); } + + @Test + void testImportModule() { + String source = """ + package com.example; + + import module java.base; + + import static java.sql.Connection.TRANSACTION_NONE; + + public class Source { + List xs = new ArrayList<>(); + Set ys = new HashSet<>(); + } + """; + + JavaTree.CompilationUnitTreeImpl cu = test(source); + + JavaTree.ImportTreeImpl importModule = (JavaTree.ImportTreeImpl) cu.imports().get(0); + + assertThat(importModule.isModule()).isTrue(); + assertThat(importModule.isStatic()).isFalse(); + assertThat(importModule.symbol()).isNull(); + + MemberSelectExpressionTree javaBase = (MemberSelectExpressionTree) importModule.qualifiedIdentifier(); + assertThat(javaBase.identifier().name()).isEqualTo("base"); + + assertThat(importModule.moduleKeyword().range()).isEqualTo( + new InternalRange(Position.at(3, 8), Position.at(3, 14)) + ); + + JavaTree.ImportTreeImpl importConst = (JavaTree.ImportTreeImpl) cu.imports().get(1); + assertThat(importConst.isModule()).isFalse(); + } } diff --git a/java-frontend/src/test/java/org/sonar/java/model/JWarningTest.java b/java-frontend/src/test/java/org/sonar/java/model/JWarningTest.java index 501c6b27a97..22ec9f19850 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JWarningTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JWarningTest.java @@ -201,7 +201,7 @@ void test_matchesTreeExactly() { private static ImportTree importTree(int startLine, int startColumn, int endLine, int endColumn) { InternalSyntaxToken fakeStartToken = syntaxToken(startLine, startColumn, " "); InternalSyntaxToken fakeEndToken = syntaxToken(endLine, endColumn, " "); - return new JavaTree.ImportTreeImpl(fakeStartToken, null, null, fakeEndToken); + return new JavaTree.ImportTreeImpl(fakeStartToken, null, null, null, fakeEndToken); } private static VariableTree variableTree(int startLine, int startColumn, int endLine, int endColumn) {