]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19197 Update ES and web services with code variants
authorEric Giffon <eric.giffon@sonarsource.com>
Fri, 5 May 2023 14:09:51 +0000 (16:09 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 16 May 2023 20:02:49 +0000 (20:02 +0000)
Co-authored-by: Antoine Vinot <antoine.vinot@sonarsource.com>
26 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/SearchRequestTest.java
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java
server/sonar-webserver-webapi/src/it/resources/org/sonar/server/issue/ws/SearchActionIT/search_by_variants_with_facets.json [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/show-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
sonar-ws/src/main/protobuf/ws-hotspots.proto
sonar-ws/src/main/protobuf/ws-issues.proto

index d36c839314b85913ddc8818a1fe2e557355fbf18..81104ebd1756ab905e2736cad2ba17acf300a640 100644 (file)
@@ -50,7 +50,7 @@ public final class IssueDto implements Serializable {
   public static final int AUTHOR_MAX_SIZE = 255;
   private static final char STRING_LIST_SEPARATOR = ',';
   private static final Joiner STRING_LIST_JOINER = Joiner.on(STRING_LIST_SEPARATOR).skipNulls();
-  private static final Splitter STRING_LIST_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
+  private static final Splitter STRING_LIST_SPLITTER = Splitter.on(STRING_LIST_SEPARATOR).trimResults().omitEmptyStrings();
 
   private int type;
   private String kee;
index cb16c2e28f366e31e8a1d8c71b5a8254e322f105..7f4e61bad924505abeb78bc3d2079263ee6d5e37 100644 (file)
@@ -68,7 +68,8 @@ public class IssueIteratorFactoryIT {
         .setIssueCreationDate(new Date(1115848800000L))
         .setIssueUpdateDate(new Date(1356994800000L))
         .setIssueCloseDate(null)
-        .setType(2));
+        .setType(2)
+        .setCodeVariants(List.of("variant1", "variant2")));
 
     Map<String, IssueDoc> issuesByKey = issuesByKey();
 
@@ -90,6 +91,7 @@ public class IssueIteratorFactoryIT {
     assertThat(issue.getTags()).containsOnly("tag1", "tag2", "tag3");
     assertThat(issue.effort().toMinutes()).isPositive();
     assertThat(issue.type().getDbConstant()).isEqualTo(2);
+    assertThat(issue.getCodeVariants()).containsOnly("variant1", "variant2");
   }
 
   @Test
index 40c9182facc960a8c0835adc0c238b71c44186c3..c4a0e3e59d6ab5b2428b1f7fc5d937fbdd393380 100644 (file)
@@ -69,8 +69,8 @@ public class SearchRequest {
   private List<String> sonarsourceSecurity;
   private List<String> cwe;
   private String timeZone;
-
   private Integer owaspAsvsLevel;
+  private List<String> codeVariants;
 
   public SearchRequest() {
     // nothing to do here
@@ -502,4 +502,14 @@ public class SearchRequest {
     this.owaspAsvsLevel = owaspAsvsLevel;
     return this;
   }
+
+  @CheckForNull
+  public List<String> getCodeVariants() {
+    return codeVariants;
+  }
+
+  public SearchRequest setCodeVariants(@Nullable List<String> codeVariants) {
+    this.codeVariants = codeVariants;
+    return this;
+  }
 }
index a490dc62a44b47577d04950692374220fe756813..c02ff1f7782530276241c711b0e8404e2b72867e 100644 (file)
@@ -366,4 +366,14 @@ public class IssueDoc extends BaseDoc {
     setField(IssueIndexDefinition.FIELD_ISSUE_NEW_CODE_REFERENCE, b);
     return this;
   }
+
+  @CheckForNull
+  public Collection<String> getCodeVariants() {
+    return getNullableField(IssueIndexDefinition.FIELD_ISSUE_CODE_VARIANTS);
+  }
+
+  public IssueDoc setCodeVariants(@Nullable Collection<String> codeVariants) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_CODE_VARIANTS, codeVariants);
+    return this;
+  }
 }
index 65368181f95fbfaac043b348c2366da73bd964f3..9cfb992b2a26ad83cf8daadbf0676226ef3122ee 100644 (file)
@@ -105,6 +105,7 @@ public class IssueIndexDefinition implements IndexDefinition {
   public static final String FIELD_ISSUE_CWE = "cwe";
   public static final String FIELD_ISSUE_SQ_SECURITY_CATEGORY = "sonarsourceSecurity";
   public static final String FIELD_ISSUE_VULNERABILITY_PROBABILITY = "vulnerabilityProbability";
+  public static final String FIELD_ISSUE_CODE_VARIANTS = "codeVariants";
 
   /**
    * Whether issue is new code for a branch using the reference branch new code definition.
@@ -177,5 +178,6 @@ public class IssueIndexDefinition implements IndexDefinition {
     mapping.keywordFieldBuilder(FIELD_ISSUE_SQ_SECURITY_CATEGORY).disableNorms().build();
     mapping.keywordFieldBuilder(FIELD_ISSUE_VULNERABILITY_PROBABILITY).disableNorms().build();
     mapping.createBooleanField(FIELD_ISSUE_NEW_CODE_REFERENCE);
+    mapping.keywordFieldBuilder(FIELD_ISSUE_CODE_VARIANTS).disableNorms().build();
   }
 }
index 907d1d55465dcbd1df24f830d35b78d374d42b84..213a55599e16246018b1cbdaaa1c543e39e58f5a 100644 (file)
@@ -82,7 +82,8 @@ class IssueIteratorForSingleChunk implements IssueIterator {
     "i.issue_type",
     "r.security_standards",
     "c.qualifier",
-    "n.uuid"
+    "n.uuid",
+    "i.code_variants"
   };
 
   private static final String SQL_ALL = "select " + StringUtils.join(FIELDS, ",") + " from issues i " +
@@ -96,7 +97,7 @@ class IssueIteratorForSingleChunk implements IssueIterator {
   private static final String ISSUE_KEY_FILTER_PREFIX = " and i.kee in (";
   private static final String ISSUE_KEY_FILTER_SUFFIX = ") ";
 
-  static final Splitter TAGS_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
+  static final Splitter STRING_LIST_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
 
   private final DbSession session;
 
@@ -222,7 +223,7 @@ class IssueIteratorForSingleChunk implements IssueIterator {
       doc.setIsMainBranch(isMainBranch);
       doc.setProjectUuid(projectUuid);
       String tags = rs.getString(20);
-      doc.setTags(IssueIteratorForSingleChunk.TAGS_SPLITTER.splitToList(tags == null ? "" : tags));
+      doc.setTags(STRING_LIST_SPLITTER.splitToList(tags == null ? "" : tags));
       doc.setType(RuleType.valueOf(rs.getInt(21)));
 
       SecurityStandards securityStandards = fromSecurityStandards(deserializeSecurityStandardsString(rs.getString(22)));
@@ -239,6 +240,8 @@ class IssueIteratorForSingleChunk implements IssueIterator {
 
       doc.setScope(Qualifiers.UNIT_TEST_FILE.equals(rs.getString(23)) ? IssueScope.TEST : IssueScope.MAIN);
       doc.setIsNewCodeReference(!isNullOrEmpty(rs.getString(24)));
+      String codeVariants = rs.getString(25);
+      doc.setCodeVariants(STRING_LIST_SPLITTER.splitToList(codeVariants == null ? "" : codeVariants));
       return doc;
     }
 
index a5550ce1bb820a537c114b67f8aa3bc6c477cdb9..04ef1b6348846a5d2614513cf93b3a677d398310 100644 (file)
@@ -53,7 +53,8 @@ public class SearchRequestTest {
       .setOwaspAsvs40(asList("1.1.1", "4.2.2"))
       .setOwaspAsvsLevel(2)
       .setPciDss32(asList("1", "4"))
-      .setPciDss40(asList("3", "5"));
+      .setPciDss40(asList("3", "5"))
+      .setCodeVariants(asList("variant1", "variant2"));
 
     assertThat(underTest.getIssues()).containsOnlyOnce("anIssueKey");
     assertThat(underTest.getSeverities()).containsExactly("MAJOR", "MINOR");
@@ -79,6 +80,7 @@ public class SearchRequestTest {
     assertThat(underTest.getOwaspAsvsLevel()).isEqualTo(2);
     assertThat(underTest.getPciDss32()).containsExactly("1", "4");
     assertThat(underTest.getPciDss40()).containsExactly("3", "5");
+    assertThat(underTest.getCodeVariants()).containsExactly("variant1", "variant2");
   }
 
   @Test
index 1cc371ca21bd9e6d8a61002836f109f1dfcd6170..7b38a2ccb119a56b951157125033b5c2ac6bb8ee 100644 (file)
@@ -117,6 +117,7 @@ import static org.sonar.server.es.searchrequest.TopAggregationHelper.NO_OTHER_SU
 import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNED_TO_ME;
 import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNEES;
 import static org.sonar.server.issue.index.IssueIndex.Facet.AUTHOR;
+import static org.sonar.server.issue.index.IssueIndex.Facet.CODE_VARIANTS;
 import static org.sonar.server.issue.index.IssueIndex.Facet.CREATED_AT;
 import static org.sonar.server.issue.index.IssueIndex.Facet.CWE;
 import static org.sonar.server.issue.index.IssueIndex.Facet.DIRECTORIES;
@@ -140,6 +141,7 @@ import static org.sonar.server.issue.index.IssueIndex.Facet.TYPES;
 import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID;
 import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN;
 import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID;
+import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_CODE_VARIANTS;
 import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID;
 import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_CWE;
 import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH;
@@ -179,6 +181,7 @@ import static org.sonar.server.view.index.ViewIndexDefinition.TYPE_VIEW;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHOR;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CODE_VARIANTS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AT;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CWE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES;
@@ -259,7 +262,8 @@ public class IssueIndex {
     SANS_TOP_25(PARAM_SANS_TOP_25, FIELD_ISSUE_SANS_TOP_25, STICKY, DEFAULT_FACET_SIZE),
     CWE(PARAM_CWE, FIELD_ISSUE_CWE, STICKY, DEFAULT_FACET_SIZE),
     CREATED_AT(PARAM_CREATED_AT, FIELD_ISSUE_FUNC_CREATED_AT, NON_STICKY),
-    SONARSOURCE_SECURITY(PARAM_SONARSOURCE_SECURITY, FIELD_ISSUE_SQ_SECURITY_CATEGORY, STICKY, DEFAULT_FACET_SIZE);
+    SONARSOURCE_SECURITY(PARAM_SONARSOURCE_SECURITY, FIELD_ISSUE_SQ_SECURITY_CATEGORY, STICKY, DEFAULT_FACET_SIZE),
+    CODE_VARIANTS(PARAM_CODE_VARIANTS, FIELD_ISSUE_CODE_VARIANTS, STICKY, MAX_FACET_SIZE);
 
     private final String name;
     private final SimpleFieldTopAggregationDefinition topAggregation;
@@ -452,6 +456,7 @@ public class IssueIndex {
         FIELD_ISSUE_RULE_UUID,
         query.ruleUuids()));
     filters.addFilter(FIELD_ISSUE_STATUS, STATUSES.getFilterScope(), createTermsFilter(FIELD_ISSUE_STATUS, query.statuses()));
+    filters.addFilter(FIELD_ISSUE_CODE_VARIANTS, CODE_VARIANTS.getFilterScope(), createTermsFilter(FIELD_ISSUE_CODE_VARIANTS, query.codeVariants()));
 
     // security category
     addSecurityCategoryPrefixFilter(FIELD_ISSUE_PCI_DSS_32, PCI_DSS_32, query.pciDss32(), filters);
@@ -784,6 +789,7 @@ public class IssueIndex {
     addFacetIfNeeded(options, aggregationHelper, esRequest, AUTHOR, query.authors().toArray());
     addFacetIfNeeded(options, aggregationHelper, esRequest, TAGS, query.tags().toArray());
     addFacetIfNeeded(options, aggregationHelper, esRequest, TYPES, query.types().toArray());
+    addFacetIfNeeded(options, aggregationHelper, esRequest, CODE_VARIANTS, query.codeVariants().toArray());
 
     addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_32, PCI_DSS_32, options, aggregationHelper, esRequest, query.pciDss32().toArray());
     addSecurityCategoryFacetIfNeeded(PARAM_PCI_DSS_40, PCI_DSS_40, options, aggregationHelper, esRequest, query.pciDss40().toArray());
index fd73568089ff323310304a0dfedb685d12ce39b1..58174b3b5f2791369f08e62cc24799f7c8252d89 100644 (file)
@@ -98,6 +98,7 @@ public class IssueQuery {
   private final ZoneId timeZone;
   private final Boolean newCodeOnReference;
   private final Collection<String> newCodeOnReferenceByProjectUuids;
+  private final Collection<String> codeVariants;
 
   private IssueQuery(Builder builder) {
     this.issueKeys = defaultCollection(builder.issueKeys);
@@ -141,6 +142,7 @@ public class IssueQuery {
     this.timeZone = builder.timeZone;
     this.newCodeOnReference = builder.newCodeOnReference;
     this.newCodeOnReferenceByProjectUuids = defaultCollection(builder.newCodeOnReferenceByProjectUuids);
+    this.codeVariants = defaultCollection(builder.codeVariants);
   }
 
   public Collection<String> issueKeys() {
@@ -328,6 +330,9 @@ public class IssueQuery {
     return newCodeOnReferenceByProjectUuids;
   }
 
+  public Collection<String> codeVariants() {
+    return codeVariants;
+  }
 
   public static class Builder {
     private Collection<String> issueKeys;
@@ -371,6 +376,7 @@ public class IssueQuery {
     private ZoneId timeZone;
     private Boolean newCodeOnReference = null;
     private Collection<String> newCodeOnReferenceByProjectUuids;
+    private Collection<String> codeVariants;
 
     private Builder() {
 
@@ -607,6 +613,11 @@ public class IssueQuery {
       this.newCodeOnReferenceByProjectUuids = newCodeOnReferenceByProjectUuids;
       return this;
     }
+
+    public Builder codeVariants(@Nullable Collection<String> codeVariants) {
+      this.codeVariants = codeVariants;
+      return this;
+    }
   }
 
   private static <T> Collection<T> defaultCollection(@Nullable Collection<T> c) {
index b2645b4d6dc59c767fcde0fd9dfa218ed52f3ccd..7455abdbee1ef7c5947a8169844fde8670c2d236 100644 (file)
@@ -151,7 +151,8 @@ public class IssueQueryFactory {
         .createdAt(parseStartingDateOrDateTime(request.getCreatedAt(), timeZone))
         .createdBefore(parseEndingDateOrDateTime(request.getCreatedBefore(), timeZone))
         .facetMode(request.getFacetMode())
-        .timeZone(timeZone);
+        .timeZone(timeZone)
+        .codeVariants(request.getCodeVariants());
 
       List<ComponentDto> allComponents = new ArrayList<>();
       boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession, request, allComponents);
index ad2a99e4568af227181ea7e2001f6ad811c472d0..00046aa60122208f1b7f62e36d484046b40368bf 100644 (file)
@@ -644,6 +644,23 @@ public class IssueIndexFacetsTest extends IssueIndexTestCommon {
     assertThat(createdAt).isNull();
   }
 
+  @Test
+  public void search_shouldReturnCodeVariantsFacet() {
+    ComponentDto project = newPrivateProjectDto();
+    ComponentDto file = newFileDto(project);
+
+    indexIssues(
+      newDoc("I1", project.uuid(), file).setCodeVariants(asList("variant1", "variant2")),
+      newDoc("I2", project.uuid(), file).setCodeVariants(singletonList("variant2")),
+      newDoc("I3", project.uuid(), file).setCodeVariants(singletonList("variant3")),
+      newDoc("I4", project.uuid(), file));
+
+    assertThatFacetHasOnly(IssueQuery.builder(), "codeVariants",
+      entry("variant1", 1L),
+      entry("variant2", 2L),
+      entry("variant3", 1L));
+  }
+
   private SearchOptions fixtureForCreatedAtFacet() {
     ComponentDto project = newPrivateProjectDto();
     ComponentDto file = newFileDto(project);
index fed30d63c1881dbd5b51392ffbeb8082d4b09584..9073102176659b8f8e06366059df1726fa252bce 100644 (file)
@@ -838,6 +838,20 @@ public class IssueIndexFiltersTest extends IssueIndexTestCommon {
     assertThatSearchReturnsOnly(IssueQuery.builder().sonarsourceSecurity(singletonList("buffer-overflow")), "I1");
   }
 
+  @Test
+  public void search_whenFilteringByCodeVariants_shouldReturnRelevantIssues() {
+    ComponentDto project = newPrivateProjectDto();
+    ComponentDto file = newFileDto(project);
+
+    indexIssues(
+      newDoc("I1", project.uuid(), file).setCodeVariants(asList("variant1", "variant2")),
+      newDoc("I2", project.uuid(), file).setCodeVariants(singletonList("variant2")),
+      newDoc("I3", project.uuid(), file).setCodeVariants(singletonList("variant3")),
+      newDoc("I4", project.uuid(), file));
+
+    assertThatSearchReturnsOnly(IssueQuery.builder().codeVariants(singletonList("variant2")), "I1", "I2");
+  }
+
   private void indexView(String viewUuid, List<String> projects) {
     viewIndexer.index(new ViewDoc().setUuid(viewUuid).setProjects(projects));
   }
index 219f54b7c1f30f478172be8a8c9febf128bee553..ececa35a00c79487869040112de573619824f558 100644 (file)
@@ -104,7 +104,8 @@ public class IssueQueryFactoryTest {
       .setCreatedBefore("2013-04-17T09:08:24+0200")
       .setRules(asList(rule1.getKey().toString(), rule2.getKey().toString()))
       .setSort("CREATION_DATE")
-      .setAsc(true);
+      .setAsc(true)
+      .setCodeVariants(asList("variant1", "variant2"));
 
     IssueQuery query = underTest.create(request);
 
@@ -129,7 +130,7 @@ public class IssueQueryFactoryTest {
     assertThat(query.createdBefore()).isEqualTo(parseDateTime("2013-04-17T09:08:24+0200"));
     assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE);
     assertThat(query.asc()).isTrue();
-
+    assertThat(query.codeVariants()).containsOnly("variant1", "variant2");
   }
 
   @Test
index 4c22e2d473dd8ee854ba4e6e53d8cf80e86ec103..d80c28a596c20e67e4541198f37906716b23be5d 100644 (file)
@@ -63,6 +63,7 @@ public class IssueQueryTest {
       .newCodeOnReferenceByProjectUuids(List.of("PROJECT"))
       .sort(IssueQuery.SORT_BY_CREATION_DATE)
       .asc(true)
+      .codeVariants(List.of("codeVariant1", "codeVariant2"))
       .build();
     assertThat(query.issueKeys()).containsOnly("ABCDE");
     assertThat(query.severities()).containsOnly(Severity.BLOCKER);
@@ -88,6 +89,7 @@ public class IssueQueryTest {
     assertThat(query.newCodeOnReferenceByProjectUuids()).containsOnly("PROJECT");
     assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE);
     assertThat(query.asc()).isTrue();
+    assertThat(query.codeVariants()).containsOnly("codeVariant1", "codeVariant2");
   }
 
   @Test
index e3784bc6339202aa18876626d2650ea97b0fb1fe..88a33c88b68b3d4b60d604945f3b743b78b18d72 100644 (file)
@@ -1069,6 +1069,21 @@ public class ShowActionIT {
       .containsOnly(tuple(author.getLogin(), author.getName(), author.isActive()));
   }
 
+  @Test
+  public void response_shouldContainCodeVariants() {
+    ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
+    userSessionRule.registerComponents(project);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDto rule = newRule(SECURITY_HOTSPOT);
+    IssueDto hotspot = dbTester.issues().insertHotspot(rule, project, file, t -> t.setCodeVariants(List.of("variant1", "variant2")));
+    mockChangelogAndCommentsFormattingContext();
+
+    Hotspots.ShowWsResponse response = newRequest(hotspot)
+      .executeProtobuf(Hotspots.ShowWsResponse.class);
+
+    assertThat(response.getCodeVariantsList()).containsOnly("variant1", "variant2");
+  }
+
   @Test
   public void verify_response_example() {
     ComponentDto project = dbTester.components().insertPublicProject(componentDto -> componentDto
@@ -1103,7 +1118,8 @@ public class ShowActionIT {
       .setIssueUpdateTime(time)
       .setAuthorLogin(author.getLogin())
       .setAssigneeUuid(author.getUuid())
-      .setKee("AW9mgJw6eFC3pGl94Wrf"));
+      .setKee("AW9mgJw6eFC3pGl94Wrf")
+      .setCodeVariants(List.of("windows", "linux")));
 
     List<Common.Changelog> changelog = IntStream.range(0, 3)
       .mapToObj(i -> Common.Changelog.newBuilder()
index feb7555d0a07f4a30c6de772dcbd10bb32ab7c96..591fc7a23cfaa9bd7195235e0571fffe9f137d9f 100644 (file)
@@ -116,6 +116,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_ASSIGN;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_TAGS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ADDITIONAL_FIELDS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CODE_VARIANTS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AFTER;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_HIDE_COMMENTS;
@@ -178,7 +179,8 @@ public class SearchActionIT {
       .setAssigneeUuid(simon.getUuid())
       .setTags(asList("bug", "owasp"))
       .setIssueCreationDate(parseDate("2014-09-03"))
-      .setIssueUpdateDate(parseDate("2017-12-04")));
+      .setIssueUpdateDate(parseDate("2017-12-04"))
+      .setCodeVariants(List.of("variant1", "variant2")));
     indexPermissionsAndIssues();
 
     SearchWsResponse response = ws.newRequest()
@@ -188,12 +190,12 @@ public class SearchActionIT {
       .extracting(
         Issue::getKey, Issue::getRule, Issue::getSeverity, Issue::getComponent, Issue::getResolution, Issue::getStatus, Issue::getMessage, Issue::getMessageFormattingsList,
         Issue::getEffort, Issue::getAssignee, Issue::getAuthor, Issue::getLine, Issue::getHash, Issue::getTagsList, Issue::getCreationDate, Issue::getUpdateDate,
-        Issue::getQuickFixAvailable)
+        Issue::getQuickFixAvailable, Issue::getCodeVariantsList)
       .containsExactlyInAnyOrder(
         tuple(issue.getKey(), rule.getKey().toString(), Severity.MAJOR, file.getKey(), RESOLUTION_FIXED, STATUS_RESOLVED, "the message",
           MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), "10min",
           simon.getLogin(), "John", 42, "a227e508d6646b55a086ee11d63b21e9", asList("bug", "owasp"), formatDateTime(issue.getIssueCreationDate()),
-          formatDateTime(issue.getIssueUpdateDate()), false));
+          formatDateTime(issue.getIssueUpdateDate()), false, List.of("variant1", "variant2")));
   }
 
   @Test
@@ -550,6 +552,24 @@ public class SearchActionIT {
     execute.assertJson(this.getClass(), "no_issue.json");
   }
 
+  @Test
+  public void search_by_variants_with_facets() {
+    RuleDto rule = newIssueRule();
+    ComponentDto project = db.components().insertPublicProject("PROJECT_ID", c -> c.setKey("PROJECT_KEY").setLanguage("java")).getMainBranchComponent();
+    ComponentDto file = db.components().insertComponent(newFileDto(project, null, "FILE_ID").setKey("FILE_KEY").setLanguage("java"));
+    db.issues().insertIssue(rule, project, file, i -> i.setCodeVariants(List.of("variant1")));
+    db.issues().insertIssue(rule, project, file, i -> i.setCodeVariants(List.of("variant2")));
+    db.issues().insertIssue(rule, project, file, i -> i.setCodeVariants(List.of("variant1", "variant2")));
+    db.issues().insertIssue(rule, project, file, i -> i.setCodeVariants(List.of("variant2", "variant3")));
+    indexPermissionsAndIssues();
+
+    ws.newRequest()
+      .setParam(PARAM_CODE_VARIANTS, "variant2,variant3")
+      .setParam(FACETS, PARAM_CODE_VARIANTS)
+      .execute()
+      .assertJson(this.getClass(), "search_by_variants_with_facets.json");
+  }
+
   @Test
   public void issue_on_removed_file() {
     RuleDto rule = newIssueRule();
@@ -1754,7 +1774,7 @@ public class SearchActionIT {
       "createdBefore", "createdInLast", "directories", "facets", "files", "issues", "scopes", "languages", "onComponentOnly",
       "p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "statuses", "tags", "types", "pciDss-3.2", "pciDss-4.0", "owaspAsvs-4.0",
       "owaspAsvsLevel", "owaspTop10",
-      "owaspTop10-2021", "sansTop25", "cwe", "sonarsourceSecurity", "timeZone", "inNewCodePeriod");
+      "owaspTop10-2021", "sansTop25", "cwe", "sonarsourceSecurity", "timeZone", "inNewCodePeriod", "codeVariants");
 
     WebService.Param branch = def.param(PARAM_BRANCH);
     assertThat(branch.isInternal()).isFalse();
diff --git a/server/sonar-webserver-webapi/src/it/resources/org/sonar/server/issue/ws/SearchActionIT/search_by_variants_with_facets.json b/server/sonar-webserver-webapi/src/it/resources/org/sonar/server/issue/ws/SearchActionIT/search_by_variants_with_facets.json
new file mode 100644 (file)
index 0000000..3798206
--- /dev/null
@@ -0,0 +1,89 @@
+{
+  "total": 3,
+  "p": 1,
+  "ps": 100,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 3
+  },
+  "issues": [
+    {
+      "rule": "xoo:x1",
+      "component": "FILE_KEY",
+      "project": "PROJECT_KEY",
+      "flows": [],
+      "status": "OPEN",
+      "scope": "MAIN",
+      "quickFixAvailable": false,
+      "messageFormattings": [],
+      "codeVariants": [
+        "variant2",
+        "variant3"
+      ]
+    },
+    {
+      "rule": "xoo:x1",
+      "component": "FILE_KEY",
+      "project": "PROJECT_KEY",
+      "flows": [],
+      "status": "OPEN",
+      "scope": "MAIN",
+      "quickFixAvailable": false,
+      "messageFormattings": [],
+      "codeVariants": [
+        "variant2"
+      ]
+    },
+    {
+      "rule": "xoo:x1",
+      "component": "FILE_KEY",
+      "project": "PROJECT_KEY",
+      "flows": [],
+      "status": "OPEN",
+      "scope": "MAIN",
+      "quickFixAvailable": false,
+      "messageFormattings": [],
+      "codeVariants": [
+        "variant1",
+        "variant2"
+      ]
+    }
+  ],
+  "components": [
+    {
+      "key": "FILE_KEY",
+      "enabled": true,
+      "qualifier": "FIL",
+      "name": "NAME_FILE_ID",
+      "longName": "null/NAME_FILE_ID",
+      "path": "null/NAME_FILE_ID"
+    },
+    {
+      "key": "PROJECT_KEY",
+      "enabled": true,
+      "qualifier": "TRK",
+      "name": "NAME_PROJECT_ID",
+      "longName": "LONG_NAME_PROJECT_ID"
+    }
+  ],
+  "facets": [
+    {
+      "property": "codeVariants",
+      "values": [
+        {
+          "val": "variant2",
+          "count": 3
+        },
+        {
+          "val": "variant1",
+          "count": 2
+        },
+        {
+          "val": "variant3",
+          "count": 1
+        }
+      ]
+    }
+  ]
+}
index 2740b9cb490c2432e64b49ef0437f0bd49540c83..f5c82402406730947250e3250608dc9ad2bd0111 100644 (file)
@@ -108,6 +108,7 @@ public class ShowAction implements HotspotsWsAction {
       .setDescription("Provides the details of a Security Hotspot.")
       .setSince("8.1")
       .setChangelog(
+        new Change("10.1", "Add the 'codeVariants' response field"),
         new Change("9.5", "The fields rule.riskDescription, rule.fixRecommendations, rule.vulnerabilityDescription of the response are deprecated."
           + " /api/rules/show endpoint should be used to fetch rule descriptions."),
         new Change("9.7", "Hotspot flows in the response may contain a description and a type"),
@@ -171,6 +172,7 @@ public class ShowAction implements HotspotsWsAction {
     builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));
     users.getAssignee().map(UserDto::getLogin).ifPresent(builder::setAssignee);
     Optional.ofNullable(hotspot.getAuthorLogin()).ifPresent(builder::setAuthor);
+    builder.addAllCodeVariants(hotspot.getCodeVariants());
   }
 
   private void formatComponents(Components components, ShowWsResponse.Builder responseBuilder) {
index b5e9b981091bfb96c297e89d896a956c56d4ccaa..ed55f7438d32054c9946bd4767f3892f039836ed 100644 (file)
@@ -96,6 +96,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNED;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHOR;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_BRANCH;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CODE_VARIANTS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AFTER;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AT;
@@ -156,7 +157,8 @@ public class SearchAction implements IssuesWsAction {
     PARAM_SANS_TOP_25,
     PARAM_CWE,
     PARAM_CREATED_AT,
-    PARAM_SONARSOURCE_SECURITY
+    PARAM_SONARSOURCE_SECURITY,
+    PARAM_CODE_VARIANTS
   );
 
   private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. ";
@@ -193,6 +195,7 @@ public class SearchAction implements IssuesWsAction {
         + "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
       .setSince("3.6")
       .setChangelog(
+        new Change("10.1", "Add the 'codeVariants' parameter, facet and response field"),
         new Change("10.0", "Parameter 'sansTop25' is deprecated"),
         new Change("10.0", "The value 'sansTop25' for the parameter 'facets' has been deprecated"),
         new Change("10.0", format("Deprecated value 'ASSIGNEE' in parameter '%s' is dropped", Param.SORT)),
@@ -278,7 +281,7 @@ public class SearchAction implements IssuesWsAction {
     action.createParam(PARAM_OWASP_ASVS_LEVEL)
       .setDescription("Level of OWASP ASVS categories.")
       .setSince("9.7")
-      .setPossibleValues(1,2,3);
+      .setPossibleValues(1, 2, 3);
     action.createParam(PARAM_PCI_DSS_32)
       .setDescription("Comma-separated list of PCI DSS v3.2 categories.")
       .setSince("9.6")
@@ -356,6 +359,10 @@ public class SearchAction implements IssuesWsAction {
       .setRequired(false)
       .setExampleValue("'Europe/Paris', 'Z' or '+02:00'")
       .setSince("8.6");
+    action.createParam(PARAM_CODE_VARIANTS)
+      .setDescription("Comma-separated list of code variants.")
+      .setExampleValue("windows,linux")
+      .setSince("10.1");
   }
 
   private static void addComponentRelatedParams(WebService.NewAction action) {
@@ -500,6 +507,7 @@ public class SearchAction implements IssuesWsAction {
     addMandatoryValuesToFacet(facets, PARAM_SANS_TOP_25, request.getSansTop25());
     addMandatoryValuesToFacet(facets, PARAM_CWE, request.getCwe());
     addMandatoryValuesToFacet(facets, PARAM_SONARSOURCE_SECURITY, request.getSonarsourceSecurity());
+    addMandatoryValuesToFacet(facets, PARAM_CODE_VARIANTS, request.getCodeVariants());
   }
 
   private static void setTypesFacet(Facets facets) {
@@ -577,7 +585,8 @@ public class SearchAction implements IssuesWsAction {
       .setSansTop25(request.paramAsStrings(PARAM_SANS_TOP_25))
       .setCwe(request.paramAsStrings(PARAM_CWE))
       .setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY))
-      .setTimeZone(request.param(PARAM_TIMEZONE));
+      .setTimeZone(request.param(PARAM_TIMEZONE))
+      .setCodeVariants(request.paramAsStrings(PARAM_CODE_VARIANTS));
   }
 
   private void checkIfNeedIssueSync(DbSession dbSession, SearchRequest searchRequest) {
index e22262d64289fbd8bf8ae21009306bc8d26c861d..75233dde0b289b46d0c4c30aeca3523a1de64804 100644 (file)
@@ -181,6 +181,7 @@ public class SearchResponseFormat {
     issueBuilder.setMessage(nullToEmpty(dto.getMessage()));
     issueBuilder.addAllMessageFormattings(MessageFormattingUtils.dbMessageFormattingToWs(dto.parseMessageFormattings()));
     issueBuilder.addAllTags(dto.getTags());
+    issueBuilder.addAllCodeVariants(dto.getCodeVariants());
     Long effort = dto.getEffort();
     if (effort != null) {
       String effortValue = durations.encode(Duration.create(effort));
index feb46663bd07d63b5dc305195c3e73c4cd0383d1..575ccea5231f42b7d53a90bf167e62580f9f08fd 100644 (file)
       "active": true
     }
   ],
-  "canChangeStatus": true
+  "canChangeStatus": true,
+  "codeVariants": [
+    "windows",
+    "linux"
+  ]
 }
index 3e90745a0622fad79aabc26057aa2614bac91c5b..24d91b687615106a5316affc0647e05d1d1154ff 100644 (file)
         }
       ],
       "quickFixAvailable": false,
-      "ruleDescriptionContextKey": "spring"
+      "ruleDescriptionContextKey": "spring",
+      "codeVariants": [
+        "windows",
+        "linux"
+      ]
     }
   ],
   "components": [
index 6f68df0a595b04396a42ee26ecd3d8673f735e46..d6687e73f6b513684a946a8a4540ddfbfa1e067c 100644 (file)
@@ -139,6 +139,7 @@ public class SearchResponseFormatFormatOperationTest {
     assertThat(issue.getCloseDate()).isEqualTo(formatDateTime(issueDto.getIssueCloseDate()));
     assertThat(issue.getQuickFixAvailable()).isEqualTo(issueDto.isQuickFixAvailable());
     assertThat(issue.getRuleDescriptionContextKey()).isEqualTo(issueDto.getOptionalRuleDescriptionContextKey().orElse(null));
+    assertThat(new ArrayList<>(issue.getCodeVariantsList())).containsExactlyInAnyOrderElementsOf(issueDto.getCodeVariants());
   }
 
   @Test
index b9f417bc9b8ad5ef0e0463285185e9f0861a77da..8791003f93f513eab57f373ec9dfc501ab6a1653 100644 (file)
@@ -102,6 +102,7 @@ public class IssuesWsParameters {
   public static final String PARAM_ASC = "asc";
   public static final String PARAM_ADDITIONAL_FIELDS = "additionalFields";
   public static final String PARAM_TIMEZONE = "timeZone";
+  public static final String PARAM_CODE_VARIANTS = "codeVariants";
 
   public static final String FACET_MODE_EFFORT = "effort";
 
index c846f04ad9e76a707b3c8f752a139a54a4b2fb6d..0e0599e42de39e3a77f59751f2e9e55f863dd1e4 100644 (file)
@@ -75,6 +75,7 @@ message ShowWsResponse {
   optional bool canChangeStatus = 17;
   repeated sonarqube.ws.commons.Flow flows = 19;
   repeated sonarqube.ws.commons.MessageFormatting messageFormattings = 20;
+  repeated string codeVariants = 21;
 }
 
 message Component {
index b03e72200b13355fb7047f96fabe5f1716f5bcf9..1956a10558a8bdba1a242cde3a21c61059d0adfd 100644 (file)
@@ -161,6 +161,8 @@ message Issue {
   optional string ruleDescriptionContextKey = 37;
 
   repeated sonarqube.ws.commons.MessageFormatting messageFormattings = 38;
+
+  repeated string codeVariants = 39;
 }
 
 message Transitions {