From ecd5c5cfdccb7b2a51f45df7c576af02ec2f63a1 Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Mon, 26 Jan 2026 16:01:47 +0100 Subject: [PATCH 1/6] SONARJAVA-5984 Support Module Import Declarations --- .../java/org/sonar/java/model/JParser.java | 27 +++++++++----- .../java/org/sonar/java/model/JavaTree.java | 16 +++++++++ .../plugins/java/api/tree/ImportTree.java | 8 +++++ .../sonar/java/model/JParserSemanticTest.java | 36 +++++++++++++++++++ .../org/sonar/java/model/JWarningTest.java | 2 +- 5 files changed, 80 insertions(+), 9 deletions(-) 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..688c8cb679e 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,31 @@ 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) { + 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..2faa1babb5c 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,25 @@ 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; private final SyntaxToken staticToken; + 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 +363,11 @@ public boolean isStatic() { return isStatic; } + @Override + public boolean isModule() { + return isModule; + } + @Override public SyntaxToken importKeyword() { return importToken; @@ -369,6 +379,12 @@ public SyntaxToken staticKeyword() { return staticToken; } + @Nullable + @Override + public SyntaxToken moduleKeyword() { + return moduleToken; + } + @Override public Tree qualifiedIdentifier() { return qualifiedIdentifier; 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..5e33d45ae0a 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,19 @@ public interface ImportTree extends ImportClauseTree { */ boolean isStatic(); + /** + * @since Java 25 + */ + boolean isModule(); + SyntaxToken importKeyword(); @Nullable SyntaxToken staticKeyword(); + @Nullable + 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..7a150a5b671 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); + + ImportTree importModule = (ImportTree) 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)) + ); + + ImportTree importConst = (ImportTree) 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) { From e6be949453f35c5b3ef819d272983679d914d342 Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Wed, 28 Jan 2026 15:33:18 +0100 Subject: [PATCH 2/6] fix ImportTreeImpl --- .../test/java/org/sonar/java/model/JParserSemanticTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7a150a5b671..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 @@ -2086,7 +2086,7 @@ public class Source { JavaTree.CompilationUnitTreeImpl cu = test(source); - ImportTree importModule = (ImportTree) cu.imports().get(0); + JavaTree.ImportTreeImpl importModule = (JavaTree.ImportTreeImpl) cu.imports().get(0); assertThat(importModule.isModule()).isTrue(); assertThat(importModule.isStatic()).isFalse(); @@ -2099,7 +2099,7 @@ public class Source { new InternalRange(Position.at(3, 8), Position.at(3, 14)) ); - ImportTree importConst = (ImportTree) cu.imports().get(1); + JavaTree.ImportTreeImpl importConst = (JavaTree.ImportTreeImpl) cu.imports().get(1); assertThat(importConst.isModule()).isFalse(); } } From 332963f3306b4ec10bbba6793d6e27b0202f2cc4 Mon Sep 17 00:00:00 2001 From: tomasz-tylenda-sonarsource Date: Mon, 2 Feb 2026 14:54:30 +0100 Subject: [PATCH 3/6] Update java-frontend/src/main/java/org/sonar/plugins/java/api/tree/ImportTree.java Co-authored-by: Dorian Burihabwa <75226315+dorian-burihabwa-sonarsource@users.noreply.github.com> --- .../main/java/org/sonar/plugins/java/api/tree/ImportTree.java | 3 +++ 1 file changed, 3 insertions(+) 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 5e33d45ae0a..2e81135ff32 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 @@ -52,6 +52,9 @@ public interface ImportTree extends ImportClauseTree { SyntaxToken staticKeyword(); @Nullable + /** + * @since Java 25 + * / SyntaxToken moduleKeyword(); Tree qualifiedIdentifier(); From e1af38bdc554c34b97f8165e97efc3a1d13b2913 Mon Sep 17 00:00:00 2001 From: tomasz-tylenda-sonarsource Date: Mon, 2 Feb 2026 15:04:08 +0100 Subject: [PATCH 4/6] Apply suggestion from @rombirli Co-authored-by: rombirli <56340680+rombirli@users.noreply.github.com> --- java-frontend/src/main/java/org/sonar/java/model/JavaTree.java | 2 ++ 1 file changed, 2 insertions(+) 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 2faa1babb5c..197a33f6e79 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 @@ -321,7 +321,9 @@ public static class ImportTreeImpl extends JavaTree implements ImportTree { 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; From 947d87c5b9858be8a5bde5c79d38f8e9506e740f Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Mon, 2 Feb 2026 15:19:00 +0100 Subject: [PATCH 5/6] review --- java-frontend/src/main/java/org/sonar/java/model/JavaTree.java | 1 + .../main/java/org/sonar/plugins/java/api/tree/ImportTree.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 197a33f6e79..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 @@ -407,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 2e81135ff32..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 @@ -54,7 +54,7 @@ public interface ImportTree extends ImportClauseTree { @Nullable /** * @since Java 25 - * / + */ SyntaxToken moduleKeyword(); Tree qualifiedIdentifier(); From 0a1ea69008f76249e8e080f3e7cc736f4d7b5aed Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Mon, 2 Feb 2026 15:28:44 +0100 Subject: [PATCH 6/6] comment --- java-frontend/src/main/java/org/sonar/java/model/JParser.java | 1 + 1 file changed, 1 insertion(+) 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 688c8cb679e..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 @@ -597,6 +597,7 @@ private JavaTree.CompilationUnitTreeImpl convertCompilationUnit(CompilationUnit ); if (!isModuleImport) { + // There is no method to resolve bindings for a module import. t.binding = e2.resolveBinding(); }