[apex] New Rule: AvoidNonRestrictiveQueries (#5089)
Merge pull request #5089 from adangel:apex-issue-635-avoid-non-restrictive-queries
This commit is contained in:
@ -16,12 +16,18 @@ This is a {{ site.pmd.release_type }} release.
|
|||||||
|
|
||||||
### 🌟 New and changed rules
|
### 🌟 New and changed rules
|
||||||
|
|
||||||
|
#### New Rules
|
||||||
|
|
||||||
|
* The new Apex rule {% apex/performance/AvoidNonRestrictiveQueries %} finds SOQL and SOSL queries without a where
|
||||||
|
or limit statement. This can quickly cause governor limit exceptions.
|
||||||
|
|
||||||
#### Changed rules
|
#### Changed rules
|
||||||
* {%rule apex/codestyle/ClassNamingConvention %}: Two new properties to configure different patterns
|
* {%rule apex/codestyle/ClassNamingConvention %}: Two new properties to configure different patterns
|
||||||
for inner classes and interfaces: `innerClassPattern` and `innerInterfacePattern`.
|
for inner classes and interfaces: `innerClassPattern` and `innerInterfacePattern`.
|
||||||
|
|
||||||
### 🐛 Fixed Issues
|
### 🐛 Fixed Issues
|
||||||
* apex-codestyle
|
* apex
|
||||||
|
* [#635](https://github.com/pmd/pmd/issues/635): \[apex] New Rule: Avoid soql/sosl queries without a where clause or limit statement
|
||||||
* [#4800](https://github.com/pmd/pmd/issues/4800): \[apex] ClassNamingConvention: Support naming convention for *inner* classes
|
* [#4800](https://github.com/pmd/pmd/issues/4800): \[apex] ClassNamingConvention: Support naming convention for *inner* classes
|
||||||
* plsql
|
* plsql
|
||||||
* [#5086](https://github.com/pmd/pmd/pull/5086): \[plsql] Fixed issue with missing optional table alias in MERGE usage
|
* [#5086](https://github.com/pmd/pmd/pull/5086): \[plsql] Fixed issue with missing optional table alias in MERGE usage
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sourceforge.pmd.lang.apex.rule.performance;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
|
||||||
|
import net.sourceforge.pmd.lang.apex.ast.ASTAnnotation;
|
||||||
|
import net.sourceforge.pmd.lang.apex.ast.ASTAnnotationParameter;
|
||||||
|
import net.sourceforge.pmd.lang.apex.ast.ASTMethod;
|
||||||
|
import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode;
|
||||||
|
import net.sourceforge.pmd.lang.apex.ast.ASTSoqlExpression;
|
||||||
|
import net.sourceforge.pmd.lang.apex.ast.ASTSoslExpression;
|
||||||
|
import net.sourceforge.pmd.lang.apex.ast.ASTUserClass;
|
||||||
|
import net.sourceforge.pmd.lang.apex.ast.ApexNode;
|
||||||
|
import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule;
|
||||||
|
import net.sourceforge.pmd.lang.ast.NodeStream;
|
||||||
|
import net.sourceforge.pmd.lang.rule.RuleTargetSelector;
|
||||||
|
import net.sourceforge.pmd.reporting.RuleContext;
|
||||||
|
|
||||||
|
public class AvoidNonRestrictiveQueriesRule extends AbstractApexRule {
|
||||||
|
private static final Pattern RESTRICTIVE_PATTERN = Pattern.compile("(where\\s+)|(limit\\s+)", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern SELECT_OR_FIND_PATTERN = Pattern.compile("(select\\s+|find\\s+)", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern SUB_QUERY_PATTERN = Pattern.compile("(?i)\\(\\s*select\\s+[^)]+\\)");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @NonNull RuleTargetSelector buildTargetSelector() {
|
||||||
|
return RuleTargetSelector.forTypes(ASTSoqlExpression.class, ASTSoslExpression.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object visit(ASTSoqlExpression node, Object data) {
|
||||||
|
visitSoqlOrSosl(node, "SOQL", node.getQuery(), asCtx(data));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object visit(ASTSoslExpression node, Object data) {
|
||||||
|
visitSoqlOrSosl(node, "SOSL", node.getQuery(), asCtx(data));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitSoqlOrSosl(ApexNode<?> node, String type, String query, RuleContext ruleContext) {
|
||||||
|
ASTMethod method = node.ancestors(ASTMethod.class).first();
|
||||||
|
if (method != null && method.getModifiers().isTest()) {
|
||||||
|
Optional<ASTAnnotation> methodAnnotation = method
|
||||||
|
.children(ASTModifierNode.class)
|
||||||
|
.children(ASTAnnotation.class)
|
||||||
|
.filter(a -> "isTest".equalsIgnoreCase(a.getName()))
|
||||||
|
.firstOpt();
|
||||||
|
|
||||||
|
Optional<ASTAnnotation> classAnnotation = method
|
||||||
|
.ancestors(ASTUserClass.class)
|
||||||
|
.firstOpt()
|
||||||
|
.map(u -> u.children(ASTModifierNode.class))
|
||||||
|
.map(s -> s.children(ASTAnnotation.class))
|
||||||
|
.map(NodeStream::first);
|
||||||
|
|
||||||
|
Optional<Boolean> methodSeeAllData = methodAnnotation.flatMap(m -> m.children(ASTAnnotationParameter.class)
|
||||||
|
.filter(p -> ASTAnnotationParameter.SEE_ALL_DATA.equalsIgnoreCase(p.getName()))
|
||||||
|
.firstOpt()
|
||||||
|
.map(ASTAnnotationParameter::getBooleanValue));
|
||||||
|
boolean classSeeAllData = classAnnotation.flatMap(m -> m.children(ASTAnnotationParameter.class)
|
||||||
|
.filter(p -> ASTAnnotationParameter.SEE_ALL_DATA.equalsIgnoreCase(p.getName()))
|
||||||
|
.firstOpt()
|
||||||
|
.map(ASTAnnotationParameter::getBooleanValue))
|
||||||
|
.orElse(false);
|
||||||
|
|
||||||
|
if (methodSeeAllData.isPresent()) {
|
||||||
|
if (!methodSeeAllData.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!classSeeAllData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher subQueryMatcher = SUB_QUERY_PATTERN.matcher(query);
|
||||||
|
StringBuffer queryWithoutSubQueries = new StringBuffer(query.length());
|
||||||
|
while (subQueryMatcher.find()) {
|
||||||
|
subQueryMatcher.appendReplacement(queryWithoutSubQueries, "(replaced_subquery)");
|
||||||
|
}
|
||||||
|
subQueryMatcher.appendTail(queryWithoutSubQueries);
|
||||||
|
|
||||||
|
verifyQuery(ruleContext, node, type, queryWithoutSubQueries.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyQuery(RuleContext ctx, ApexNode<?> node, String type, String query) {
|
||||||
|
int occurrencesSelectOrFind = countOccurrences(SELECT_OR_FIND_PATTERN, query);
|
||||||
|
int occurrencesWhereOrLimit = countOccurrences(RESTRICTIVE_PATTERN, query);
|
||||||
|
|
||||||
|
if (occurrencesSelectOrFind > 0 && occurrencesWhereOrLimit == 0) {
|
||||||
|
ctx.addViolation(node, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countOccurrences(Pattern pattern, String s) {
|
||||||
|
int occurrences = 0;
|
||||||
|
Matcher matcher = pattern.matcher(s);
|
||||||
|
while (matcher.find()) {
|
||||||
|
occurrences++;
|
||||||
|
}
|
||||||
|
return occurrences;
|
||||||
|
}
|
||||||
|
}
|
@ -53,6 +53,32 @@ public class Foo {
|
|||||||
</example>
|
</example>
|
||||||
</rule>
|
</rule>
|
||||||
|
|
||||||
|
<rule name="AvoidNonRestrictiveQueries"
|
||||||
|
language="apex"
|
||||||
|
since="7.4.0"
|
||||||
|
message="Avoid {0} queries without a where or limit statement"
|
||||||
|
class="net.sourceforge.pmd.lang.apex.rule.performance.AvoidNonRestrictiveQueriesRule"
|
||||||
|
externalInfoUrl="${pmd.website.baseurl}/pmd_rules_apex_performance.html#avoidnonrestrictivequeries">
|
||||||
|
<description>
|
||||||
|
When working with very large amounts of data, unfiltered SOQL or SOSL queries can quickly cause
|
||||||
|
[governor limit](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm)
|
||||||
|
exceptions.
|
||||||
|
</description>
|
||||||
|
<priority>3</priority>
|
||||||
|
<example>
|
||||||
|
<![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static void main( String[] as ) {
|
||||||
|
Account[] accs1 = [ select id from account ]; // Bad
|
||||||
|
Account[] accs2 = [ select id from account limit 10 ]; // better
|
||||||
|
|
||||||
|
List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead]; // bad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]>
|
||||||
|
</example>
|
||||||
|
</rule>
|
||||||
|
|
||||||
<rule name="EagerlyLoadedDescribeSObjectResult"
|
<rule name="EagerlyLoadedDescribeSObjectResult"
|
||||||
language="apex"
|
language="apex"
|
||||||
since="6.40.0"
|
since="6.40.0"
|
||||||
|
@ -87,6 +87,7 @@
|
|||||||
<priority>3</priority>
|
<priority>3</priority>
|
||||||
</rule>
|
</rule>
|
||||||
<!-- <rule ref="category/apex/performance.xml/AvoidDebugStatements" /> -->
|
<!-- <rule ref="category/apex/performance.xml/AvoidDebugStatements" /> -->
|
||||||
|
<!-- <rule ref="category/apex/performance.xml/AvoidNonRestrictiveQueries"/> -->
|
||||||
<!-- <rule ref="category/apex/performance.xml/EagerlyLoadedDescribeSObjectResult" /> -->
|
<!-- <rule ref="category/apex/performance.xml/EagerlyLoadedDescribeSObjectResult" /> -->
|
||||||
|
|
||||||
<!-- NAMING -->
|
<!-- NAMING -->
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sourceforge.pmd.lang.apex.rule.performance;
|
||||||
|
|
||||||
|
import net.sourceforge.pmd.test.PmdRuleTst;
|
||||||
|
|
||||||
|
class AvoidNonRestrictiveQueriesTest extends PmdRuleTst {
|
||||||
|
// no additional unit tests
|
||||||
|
}
|
@ -0,0 +1,265 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<test-data
|
||||||
|
xmlns="http://pmd.sourceforge.net/rule-tests"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://pmd.sourceforge.net/rule-tests http://pmd.sourceforge.net/rule-tests_1_0_0.xsd">
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Query without a where statement</description>
|
||||||
|
<expected-problems>1</expected-problems>
|
||||||
|
<expected-messages>
|
||||||
|
<message>Avoid SOQL queries without a where or limit statement</message>
|
||||||
|
</expected-messages>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static void main( String[] as ) {
|
||||||
|
Account[] accs = [ select id from account ]; //Bad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Query with where and limit statement</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static void main( String[] as ) {
|
||||||
|
Account[] accs = [ select id from account where id = 1 limit 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Subquery without limit</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static void main( String[] as ) {
|
||||||
|
Account[] accs = [ select id, (SELECT Id FROM Contact) from account where id = 1]; // That's ok, as long as the main query is restricted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Multiple non restrictive queries</description>
|
||||||
|
<expected-problems>3</expected-problems>
|
||||||
|
<expected-linenumbers>3,4,5</expected-linenumbers>
|
||||||
|
<expected-messages>
|
||||||
|
<message>Avoid SOQL queries without a where or limit statement</message>
|
||||||
|
<message>Avoid SOQL queries without a where or limit statement</message>
|
||||||
|
<message>Avoid SOQL queries without a where or limit statement</message>
|
||||||
|
</expected-messages>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static Account[] main( String[] as ) {
|
||||||
|
Account[] accs = [ select id from account ];
|
||||||
|
Account[] accs2 = [ select id from account ];
|
||||||
|
return [SELECT Id FROM Contact];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Restrictive queries</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static Account[] main( String[] as ) {
|
||||||
|
Account[] accs = [ select id from account where name='foo' and id < 100 LIMIT 5 ];
|
||||||
|
Account[] accs2 = [ select id from account where id > 1 ];
|
||||||
|
return [SELECT Id FROM Contact limit 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>subquery with limit, main query without</description>
|
||||||
|
<expected-problems>1</expected-problems>
|
||||||
|
<expected-messages>
|
||||||
|
<message>Avoid SOQL queries without a where or limit statement</message>
|
||||||
|
</expected-messages>
|
||||||
|
<code><![CDATA[
|
||||||
|
class Query {
|
||||||
|
void method() {
|
||||||
|
Account[] accounts = [ select id, (SELECT Id FROM Contact where id = 1 limit 1) from account ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>main query with limit, subquery without is OK</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
class Query {
|
||||||
|
void method() {
|
||||||
|
Account[] accounts = [ select id, (SELECT Id FROM Contact) from account limit 1 ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOQL query - non-restrictive - SeeAllData=true</description>
|
||||||
|
<expected-problems>1</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
@IsTest(SeeAllData=true)
|
||||||
|
public class TestDataAccessClass {
|
||||||
|
@IsTest
|
||||||
|
static void myTestMethod() {
|
||||||
|
Account[] accounts = [SELECT Id, Name FROM Account];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOQL query - non-restrictive - SeeAllData=False is OK</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
@IsTest(SeeAllData=false)
|
||||||
|
public class TestDataAccessClass {
|
||||||
|
@IsTest
|
||||||
|
static void myTestMethod() {
|
||||||
|
Account[] accounts = [SELECT Id, Name FROM Account];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOQL query - restrictive is OK</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
@IsTest(SeeAllData=true)
|
||||||
|
public class TestDataAccessClass {
|
||||||
|
@IsTest
|
||||||
|
static void myTestMethod() {
|
||||||
|
Account[] accounts = [SELECT Id, Name FROM Account LIMIT 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOQL query - restrictive - SeeAllData=false is OK</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
@IsTest(SeeAllData=false)
|
||||||
|
public class TestDataAccessClass {
|
||||||
|
@IsTest
|
||||||
|
static void myTestMethod() {
|
||||||
|
Account[] accounts = [SELECT Id, Name FROM Account LIMIT 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOQL query - non-restrictive - SeeAllData=false on class but true on method</description>
|
||||||
|
<expected-problems>1</expected-problems>
|
||||||
|
<expected-linenumbers>5</expected-linenumbers>
|
||||||
|
<code><![CDATA[
|
||||||
|
@IsTest(SeeAllData=false)
|
||||||
|
public class TestDataAccessClass {
|
||||||
|
@IsTest(SeeAllData=true)
|
||||||
|
static void myTestMethod1() {
|
||||||
|
Account[] accounts = [SELECT Id, Name FROM Account];
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void myTestMethod2() {
|
||||||
|
Account[] accounts = [SELECT Id, Name FROM Account]; // inherits from class annotation SeeAllData=false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOQL query - non-restrictive - SeeAllData=true on class but false on method</description>
|
||||||
|
<expected-problems>1</expected-problems>
|
||||||
|
<expected-linenumbers>10</expected-linenumbers>
|
||||||
|
<code><![CDATA[
|
||||||
|
@IsTest(SeeAllData=true)
|
||||||
|
public class TestDataAccessClass {
|
||||||
|
@IsTest(SeeAllData=false)
|
||||||
|
static void myTestMethod1() {
|
||||||
|
Account[] accounts = [SELECT Id, Name FROM Account]; // that's ok, since SeeAllData=false
|
||||||
|
}
|
||||||
|
|
||||||
|
@IsTest
|
||||||
|
static void myTestMethod2() {
|
||||||
|
Account[] accounts = [SELECT Id, Name FROM Account]; // not good, inherits SeeAllData=true from class
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOSL query - missing where</description>
|
||||||
|
<expected-problems>1</expected-problems>
|
||||||
|
<expected-messages>
|
||||||
|
<message>Avoid SOSL queries without a where or limit statement</message>
|
||||||
|
</expected-messages>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static void main( String[] as ) {
|
||||||
|
List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead]; // bad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOSL query - with limit is OK</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static void main( String[] as ) {
|
||||||
|
List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead LIMIT 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>Test case with SOSL query - with where is OK</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
public static void main( String[] as ) {
|
||||||
|
List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name WHERE Name like 'test'), Contact, Opportunity, Lead];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
|
||||||
|
<test-code>
|
||||||
|
<description>False positives for SOQL queries with WHERE on multiple lines</description>
|
||||||
|
<expected-problems>0</expected-problems>
|
||||||
|
<code><![CDATA[
|
||||||
|
public class Something {
|
||||||
|
private static final String NAMESPACE_PREFIX = 'some_prefix';
|
||||||
|
private static final String PERMISSION_SET = 'some_permission';
|
||||||
|
|
||||||
|
public static Boolean hasPermission(Id userId) {
|
||||||
|
return ![
|
||||||
|
SELECT Id
|
||||||
|
FROM PermissionSetAssignment
|
||||||
|
WHERE
|
||||||
|
AssigneeId = :userId
|
||||||
|
AND PermissionSet.Name = :PERMISSION_SET
|
||||||
|
AND PermissionSet.NamespacePrefix = :NAMESPACE_PREFIX
|
||||||
|
]
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]></code>
|
||||||
|
</test-code>
|
||||||
|
</test-data>
|
Reference in New Issue
Block a user