Repository: pmd/pmd (Java)

Issue:
[apex] SOSL syntax error with WITH USER_MODE or WITH SYSTEM_MODE
**Affects PMD Version:** 
7.0.0 - 7.2.0

**Description:**

The underlying apex parser seems to believe the following SOSL query is invalid. (it deploys and runs just fine).

```apex
List<List<sObject>> records = [
                FIND :soslStringWC
                RETURNING VCC_Softphone_Directory__c(Name, Description__c, Phone_Number__c ORDER BY Name)
                WITH USER_MODE
                LIMIT 50
            ];
```

**Exception Stacktrace:**

```
[TRACE] Processing file (lang: apex+version:60): /Users/kevinpoorman/PROJECT/SomeFile.cls
line 65:21 no viable alternative at input 'WITH USER_MODE'
[INFO] Syntax error at 65:21: no viable alternative at input 'WITH USER_MODE'
line 66:16 missing ';' at 'LIMIT'
[INFO] Syntax error at 66:16: missing ';' at 'LIMIT'
line 67:12 extraneous input ']' expecting ';'
[INFO] Syntax error at 67:12: extraneous input ']' expecting ';'

```

**Code Sample demonstrating the issue:**

```apex
List<List<sObject>> records = [
                FIND :soslStringWC
                RETURNING VCC_Softphone_Directory__c(Name, Description__c, Phone_Number__c ORDER BY Name)
                WITH USER_MODE
                LIMIT 50
            ];
```

**Steps to reproduce:**

1. place the above apex snippet into an otherwise perfectly valid class
2. run pmd
3. fail!

**Running PMD through:** 

CLI. 

<!-- If relevant, also include your JDK and OS information, e.g. for ClassNotFoundException, LinkageError, reflection failures, etc. -->




Fix (patch):
diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md
index b81538f603f..8eb3f60fb31 100644
--- a/docs/pages/release_notes.md
+++ b/docs/pages/release_notes.md
@@ -18,7 +18,9 @@ This is a {{ site.pmd.release_type }} release.
 * core
   * [#4992](https://github.com/pmd/pmd/pull/4992): \[core] CPD: Include processing errors in XML report
 * apex
+  * [#4922](https://github.com/pmd/pmd/issues/4922): \[apex] SOQL syntax error with TYPEOF in sub-query
   * [#5053](https://github.com/pmd/pmd/issues/5053): \[apex] CPD fails to parse string literals with escaped characters
+  * [#5055](https://github.com/pmd/pmd/issues/5055): \[apex] SOSL syntax error with WITH USER_MODE or WITH SYSTEM_MODE
 * java-bestpractices
   * [#5047](https://github.com/pmd/pmd/issues/5047): \[java] UnusedPrivateMethod FP for Generics & Overloads
 * plsql
diff --git a/pmd-apex/pom.xml b/pmd-apex/pom.xml
index 6346360e1db..e3b9f32e276 100644
--- a/pmd-apex/pom.xml
+++ b/pmd-apex/pom.xml
@@ -95,16 +95,15 @@
         <dependency>
             <groupId>com.google.summit</groupId>
             <artifactId>summit-ast</artifactId>
-            <version>2.2.0</version>
+            <version>2.3.0</version>
         </dependency>
 
         <dependency>
-            <groupId>com.github.nawforce</groupId>
-            <artifactId>apexlink</artifactId>
-            <version>2.3.7</version>
+            <groupId>io.github.apex-dev-tools</groupId>
+            <artifactId>apex-ls_2.13</artifactId>
+            <version>5.2.0</version>
         </dependency>
 
-
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTApexFile.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTApexFile.java
index b280c988016..8a32161479a 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTApexFile.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTApexFile.java
@@ -18,7 +18,7 @@
 import net.sourceforge.pmd.lang.document.TextRegion;
 
 import com.google.summit.ast.CompilationUnit;
-import com.nawforce.pkgforce.api.Issue;
+import io.github.apexdevtools.api.Issue;
 
 public final class ASTApexFile extends AbstractApexNode.Single<CompilationUnit> implements RootNode {
 
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSoqlExpression.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSoqlExpression.java
index d2708662f16..8ec31a42cdc 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSoqlExpression.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSoqlExpression.java
@@ -37,7 +37,7 @@ public String getCanonicalQuery() {
         return canoncialQuery;
     }
 
-    private static String convertToCanonicalQuery(String rawQuery) {
+    static String convertToCanonicalQuery(String rawQuery) {
         // note: this is a very crude way. At some point, the parsed query should be
         // provided by Summit AST. The Apex Parser already parses the SOQL/SOSL queries.
         String query = rawQuery;
@@ -45,7 +45,7 @@ private static String convertToCanonicalQuery(String rawQuery) {
         query = query.replaceAll("(?i)\\bfrom\\b", "FROM");
         query = query.replaceAll("(?i)\\bupdate\\b", "UPDATE");
         query = query.replaceAll("(?i)\\bwhere\\b", "WHERE");
-        query = query.replaceAll("(?i)\\bgroup\\b+by\\b", "GROUP BY");
+        query = query.replaceAll("(?i)\\bgroup\\b\\s+\\bby\\b", "GROUP BY");
         query = query.replaceAll("(?i)\\band\\b", "AND");
         query = query.replaceAll("(?i)\\bor\\b", "OR");
         query = query.replaceAll("(?i)\\bnot\\b", "NOT");
@@ -78,6 +78,22 @@ private static String convertToCanonicalQuery(String rawQuery) {
         query = query.replaceAll("(?i)\\bDISTANCE\\(", "DISTANCE(");
         query = query.replaceAll("(?i)\\bconverttimezone\\(", "CONVERTTIMEZONE(");
 
+        // sosl keywords
+        query = query.replaceAll("(?i)\\bfind\\b", "FIND");
+        query = query.replaceAll("(?i)\\bin\\b", "IN");
+        query = query.replaceAll("(?i)\\breturning\\b", "RETURNING");
+        query = query.replaceAll("(?i)\\bassign\\b", "ASSIGN");
+        query = query.replaceAll("(?i)\\btarget_length\\b", "TARGET_LENGTH");
+        query = query.replaceAll("(?i)\\bmetadata\\b", "METADATA");
+        query = query.replaceAll("(?i)\\blimit\\b", "LIMIT");
+        query = query.replaceAll("(?i)\\bwith\\b\\s+\\bdivision\\b", "WITH DIVISION");
+        query = query.replaceAll("(?i)\\bwith\\b\\s+\\bdata\\b\\s+\\bcategory\\b", "WITH DATA CATEGORY");
+        query = query.replaceAll("(?i)\\bwith\\b\\s+\\bsnippet\\b", "WITH SNIPPET");
+        query = query.replaceAll("(?i)\\bwith\\b\\s+\\bnetwork\\b", "WITH NETWORK");
+        query = query.replaceAll("(?i)\\bwith\\b\\s+\\bpricebookid\\b", "WITH PRICEBOOKID");
+        query = query.replaceAll("(?i)\\bwith\\b\\s+\\buser_mode\\b", "WITH USER_MODE");
+        query = query.replaceAll("(?i)\\bwith\\b\\s+\\bsystem_mode\\b", "WITH SYSTEM_MODE");
+
         // replace all binding variables with :tmpVar1 :tmpVar2...
         Pattern bindingVarPattern = Pattern.compile(":[^ :]+\\b");
         int tmpVarCounter = 1;
@@ -89,6 +105,6 @@ private static String convertToCanonicalQuery(String rawQuery) {
         }
         matcher.appendTail(normalizedVars);
 
-        return normalizedVars.toString();
+        return normalizedVars.toString().trim();
     }
 }
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSoslExpression.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSoslExpression.java
index b1af4a56f4a..8e7a4e33dbe 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSoslExpression.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ASTSoslExpression.java
@@ -4,12 +4,16 @@
 
 package net.sourceforge.pmd.lang.apex.ast;
 
+import static net.sourceforge.pmd.lang.apex.ast.ASTSoqlExpression.convertToCanonicalQuery;
+
 import com.google.summit.ast.expression.SoslExpression;
 
 public final class ASTSoslExpression extends AbstractApexNode.Single<SoslExpression> {
+    private final String canoncialQuery;
 
     ASTSoslExpression(SoslExpression soslExpression) {
         super(soslExpression);
+        canoncialQuery = convertToCanonicalQuery(soslExpression.getQuery());
     }
 
 
@@ -17,4 +21,18 @@ public final class ASTSoslExpression extends AbstractApexNode.Single<SoslExpress
     protected <P, R> R acceptApexVisitor(ApexVisitor<? super P, ? extends R> visitor, P data) {
         return visitor.visit(this, data);
     }
+
+    /**
+     * Returns the raw query as it appears in the source code.
+     */
+    public String getQuery() {
+        return node.getQuery();
+    }
+
+    /**
+     * Returns the query with the SOSL keywords normalized as uppercase.
+     */
+    public String getCanonicalQuery() {
+        return canoncialQuery;
+    }
 }
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexCommentBuilder.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexCommentBuilder.java
index 3d7f7445f7e..a135ca46030 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexCommentBuilder.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexCommentBuilder.java
@@ -21,7 +21,7 @@
 import net.sourceforge.pmd.lang.document.TextDocument;
 import net.sourceforge.pmd.lang.document.TextRegion;
 
-import com.nawforce.apexparser.ApexLexer;
+import io.github.apexdevtools.apexparser.ApexLexer;
 
 @InternalApi
 final class ApexCommentBuilder {
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexTreeBuilder.kt b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexTreeBuilder.kt
index d6672bd3908..99c505be7ea 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexTreeBuilder.kt
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ast/ApexTreeBuilder.kt
@@ -230,7 +230,7 @@ class ApexTreeBuilder(private val task: ParserTask, private val proc: ApexLangua
                 // 2. Add the expected ASTModifier child node
                 buildModifiers(emptyList()).also { it.setParent(invokeMethod) }
                 // 3. Elide the body CompoundStatement->ASTBlockStatement
-                buildChildren(node.body, parent = invokeMethod as AbstractApexNode)
+                node.body.forEach { buildChildren(it, parent = invokeMethod as AbstractApexNode) }
             } else {
                 buildChildren(node, parent = this, exclude = { it in node.modifiers })
             }
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexer.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexer.java
index 367b2097e46..2c22ce75fb4 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexer.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/cpd/ApexCpdLexer.java
@@ -16,8 +16,8 @@
 import net.sourceforge.pmd.lang.ast.impl.antlr4.AntlrTokenManager;
 import net.sourceforge.pmd.lang.document.TextDocument;
 
-import com.nawforce.apexparser.ApexLexer;
-import com.nawforce.apexparser.CaseInsensitiveInputStream;
+import io.github.apexdevtools.apexparser.ApexLexer;
+import io.github.apexdevtools.apexparser.CaseInsensitiveInputStream;
 
 public class ApexCpdLexer implements CpdLexer {
     @Override
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/multifile/ApexMultifileAnalysis.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/multifile/ApexMultifileAnalysis.java
index 85b6429bdae..4a7acb921b8 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/multifile/ApexMultifileAnalysis.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/multifile/ApexMultifileAnalysis.java
@@ -20,8 +20,8 @@
 import net.sourceforge.pmd.lang.apex.ApexLanguageProperties;
 
 import com.nawforce.apexlink.api.Org;
-import com.nawforce.pkgforce.api.Issue;
 import com.nawforce.pkgforce.diagnostics.LoggerOps;
+import io.github.apexdevtools.api.Issue;
 
 /**
  * Stores multi-file analysis data. The 'Org' here is the primary ApexLink structure for maintaining information
diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/design/UnusedMethodRule.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/design/UnusedMethodRule.java
index 2e2d38523ad..e912f343f6e 100644
--- a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/design/UnusedMethodRule.java
+++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/design/UnusedMethodRule.java
@@ -7,13 +7,15 @@
 import net.sourceforge.pmd.lang.apex.ast.ASTMethod;
 import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule;
 
+import com.nawforce.pkgforce.diagnostics.UNUSED_CATEGORY;
+
 public class UnusedMethodRule extends AbstractApexRule {
 
     @Override
     public Object visit(ASTMethod node, Object data) {
         // Check if any 'Unused' Issues align with this method
         node.getRoot().getGlobalIssues().stream()
-            .filter(issue -> "Unused".equals(issue.category()))
+            .filter(issue -> issue.rule().name().equals(UNUSED_CATEGORY.name()))
             .filter(issue -> issue.fileLocation().startLineNumber() == node.getBeginLine())
             .filter(issue -> issue.fileLocation().endLineNumber() <= node.getBeginLine())
             .forEach(issue -> asCtx(data).addViolation(node));