From 8aadda9b9bfaadc2e8b047c2c8318a43c1c8b62b Mon Sep 17 00:00:00 2001 From: Eric Giffon Date: Fri, 5 May 2023 16:09:51 +0200 Subject: [PATCH] SONAR-19197 Update ES and web services with code variants Co-authored-by: Antoine Vinot --- .../java/org/sonar/db/issue/IssueDto.java | 2 +- .../issue/index/IssueIteratorFactoryIT.java | 4 +- .../org/sonar/server/issue/SearchRequest.java | 12 ++- .../sonar/server/issue/index/IssueDoc.java | 10 +++ .../issue/index/IssueIndexDefinition.java | 2 + .../index/IssueIteratorForSingleChunk.java | 9 +- .../sonar/server/issue/SearchRequestTest.java | 4 +- .../sonar/server/issue/index/IssueIndex.java | 8 +- .../sonar/server/issue/index/IssueQuery.java | 11 +++ .../server/issue/index/IssueQueryFactory.java | 3 +- .../issue/index/IssueIndexFacetsTest.java | 17 ++++ .../issue/index/IssueIndexFiltersTest.java | 14 +++ .../issue/index/IssueQueryFactoryTest.java | 5 +- .../server/issue/index/IssueQueryTest.java | 2 + .../sonar/server/hotspot/ws/ShowActionIT.java | 18 +++- .../sonar/server/issue/ws/SearchActionIT.java | 28 +++++- .../search_by_variants_with_facets.json | 89 +++++++++++++++++++ .../sonar/server/hotspot/ws/ShowAction.java | 2 + .../sonar/server/issue/ws/SearchAction.java | 15 +++- .../server/issue/ws/SearchResponseFormat.java | 1 + .../sonar/server/hotspot/ws/show-example.json | 6 +- .../sonar/server/issue/ws/search-example.json | 6 +- ...archResponseFormatFormatOperationTest.java | 1 + .../ws/client/issue/IssuesWsParameters.java | 1 + sonar-ws/src/main/protobuf/ws-hotspots.proto | 1 + sonar-ws/src/main/protobuf/ws-issues.proto | 2 + 26 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/it/resources/org/sonar/server/issue/ws/SearchActionIT/search_by_variants_with_facets.json diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java index d36c839314b..81104ebd175 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java @@ -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; diff --git a/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java b/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java index cb16c2e28f3..7f4e61bad92 100644 --- a/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java +++ b/server/sonar-server-common/src/it/java/org/sonar/server/issue/index/IssueIteratorFactoryIT.java @@ -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 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 diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java index 40c9182facc..c4a0e3e59d6 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java @@ -69,8 +69,8 @@ public class SearchRequest { private List sonarsourceSecurity; private List cwe; private String timeZone; - private Integer owaspAsvsLevel; + private List codeVariants; public SearchRequest() { // nothing to do here @@ -502,4 +502,14 @@ public class SearchRequest { this.owaspAsvsLevel = owaspAsvsLevel; return this; } + + @CheckForNull + public List getCodeVariants() { + return codeVariants; + } + + public SearchRequest setCodeVariants(@Nullable List codeVariants) { + this.codeVariants = codeVariants; + return this; + } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java index a490dc62a44..c02ff1f7782 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueDoc.java @@ -366,4 +366,14 @@ public class IssueDoc extends BaseDoc { setField(IssueIndexDefinition.FIELD_ISSUE_NEW_CODE_REFERENCE, b); return this; } + + @CheckForNull + public Collection getCodeVariants() { + return getNullableField(IssueIndexDefinition.FIELD_ISSUE_CODE_VARIANTS); + } + + public IssueDoc setCodeVariants(@Nullable Collection codeVariants) { + setField(IssueIndexDefinition.FIELD_ISSUE_CODE_VARIANTS, codeVariants); + return this; + } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java index 65368181f95..9cfb992b2a2 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java @@ -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(); } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java index 907d1d55465..213a55599e1 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java @@ -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; } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/SearchRequestTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/SearchRequestTest.java index a5550ce1bb8..04ef1b63488 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/SearchRequestTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/SearchRequestTest.java @@ -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 diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java index 1cc371ca21b..7b38a2ccb11 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java @@ -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()); diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java index fd73568089f..58174b3b5f2 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java @@ -98,6 +98,7 @@ public class IssueQuery { private final ZoneId timeZone; private final Boolean newCodeOnReference; private final Collection newCodeOnReferenceByProjectUuids; + private final Collection 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 issueKeys() { @@ -328,6 +330,9 @@ public class IssueQuery { return newCodeOnReferenceByProjectUuids; } + public Collection codeVariants() { + return codeVariants; + } public static class Builder { private Collection issueKeys; @@ -371,6 +376,7 @@ public class IssueQuery { private ZoneId timeZone; private Boolean newCodeOnReference = null; private Collection newCodeOnReferenceByProjectUuids; + private Collection codeVariants; private Builder() { @@ -607,6 +613,11 @@ public class IssueQuery { this.newCodeOnReferenceByProjectUuids = newCodeOnReferenceByProjectUuids; return this; } + + public Builder codeVariants(@Nullable Collection codeVariants) { + this.codeVariants = codeVariants; + return this; + } } private static Collection defaultCollection(@Nullable Collection c) { diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java index b2645b4d6dc..7455abdbee1 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java @@ -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 allComponents = new ArrayList<>(); boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession, request, allComponents); diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java index ad2a99e4568..00046aa6012 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java @@ -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); diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java index fed30d63c18..90731021766 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java @@ -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 projects) { viewIndexer.index(new ViewDoc().setUuid(viewUuid).setProjects(projects)); } diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java index 219f54b7c1f..ececa35a00c 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java @@ -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 diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java index 4c22e2d473d..d80c28a596c 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java @@ -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 diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java index e3784bc6339..88a33c88b68 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ShowActionIT.java @@ -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 changelog = IntStream.range(0, 3) .mapToObj(i -> Common.Changelog.newBuilder() diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java index feb7555d0a0..591fc7a23cf 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/SearchActionIT.java @@ -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 index 00000000000..3798206a378 --- /dev/null +++ b/server/sonar-webserver-webapi/src/it/resources/org/sonar/server/issue/ws/SearchActionIT/search_by_variants_with_facets.json @@ -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 + } + ] + } + ] +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java index 2740b9cb490..f5c82402406 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java @@ -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) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java index b5e9b981091..ed55f7438d3 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java @@ -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 { + "
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) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java index e22262d6428..75233dde0b2 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java @@ -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)); diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/show-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/show-example.json index feb46663bd0..575ccea5231 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/show-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/hotspot/ws/show-example.json @@ -108,5 +108,9 @@ "active": true } ], - "canChangeStatus": true + "canChangeStatus": true, + "codeVariants": [ + "windows", + "linux" + ] } diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json index 3e90745a062..24d91b68761 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json @@ -93,7 +93,11 @@ } ], "quickFixAvailable": false, - "ruleDescriptionContextKey": "spring" + "ruleDescriptionContextKey": "spring", + "codeVariants": [ + "windows", + "linux" + ] } ], "components": [ diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java index 6f68df0a595..d6687e73f6b 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java @@ -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 diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java index b9f417bc9b8..8791003f93f 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java @@ -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"; diff --git a/sonar-ws/src/main/protobuf/ws-hotspots.proto b/sonar-ws/src/main/protobuf/ws-hotspots.proto index c846f04ad9e..0e0599e42de 100644 --- a/sonar-ws/src/main/protobuf/ws-hotspots.proto +++ b/sonar-ws/src/main/protobuf/ws-hotspots.proto @@ -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 { diff --git a/sonar-ws/src/main/protobuf/ws-issues.proto b/sonar-ws/src/main/protobuf/ws-issues.proto index b03e72200b1..1956a10558a 100644 --- a/sonar-ws/src/main/protobuf/ws-issues.proto +++ b/sonar-ws/src/main/protobuf/ws-issues.proto @@ -161,6 +161,8 @@ message Issue { optional string ruleDescriptionContextKey = 37; repeated sonarqube.ws.commons.MessageFormatting messageFormattings = 38; + + repeated string codeVariants = 39; } message Transitions { -- 2.39.5