From 4846c45adeff777c255e1728e505072779283e51 Mon Sep 17 00:00:00 2001 From: Carl Mai Date: Fri, 27 Oct 2023 21:00:58 +0200 Subject: [PATCH 1/2] feat: implement codecheck api --- .github/workflows/build.yml | 5 +- .../api/coderepository/CodeRepository.java | 5 +- .../CodeStructuredExecutor.java | 14 +++ ...eRepository.java => CodeTextExecutor.java} | 68 +++++++------- .../api/coderepository/api/CodeApi.java | 15 ---- .../coderepository/api/CodeStructuredApi.java | 14 +++ .../api/coderepository/api/CodeTextApi.java | 20 +++++ ...eComment.java => CodeTextFileComment.java} | 2 +- ...nComment.java => CodeTextMainComment.java} | 2 +- .../api/qualitytool/GlobalMetrics.java | 2 +- .../api/qualitytool/Issue.java | 7 +- .../coderepo/github/GithubCodeRepository.java | 30 ++++--- .../github/client/GithubStructuredApi.java | 89 +++++++++++++++++++ .../{GithubApi.java => GithubTextApi.java} | 37 +++----- .../core/CommentProcessor.java | 13 ++- .../org/qualityannotate/core/MainCommand.java | 11 ++- .../sonarqube/SonarqubeQualityTool.java | 26 +++--- .../sonarqube/SonarqubeQualityToolTest.java | 15 ++-- 18 files changed, 252 insertions(+), 123 deletions(-) create mode 100644 src/main/java/org/qualityannotate/api/coderepository/CodeStructuredExecutor.java rename src/main/java/org/qualityannotate/api/coderepository/{AbstractCodeRepository.java => CodeTextExecutor.java} (67%) delete mode 100644 src/main/java/org/qualityannotate/api/coderepository/api/CodeApi.java create mode 100644 src/main/java/org/qualityannotate/api/coderepository/api/CodeStructuredApi.java create mode 100644 src/main/java/org/qualityannotate/api/coderepository/api/CodeTextApi.java rename src/main/java/org/qualityannotate/api/coderepository/api/{CodeFileComment.java => CodeTextFileComment.java} (88%) rename src/main/java/org/qualityannotate/api/coderepository/api/{CodeMainComment.java => CodeTextMainComment.java} (77%) create mode 100644 src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java rename src/main/java/org/qualityannotate/coderepo/github/client/{GithubApi.java => GithubTextApi.java} (75%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64f97e8..452cdd4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,8 +28,6 @@ on: # yamllint disable-line rule:truthy jobs: build: name: "build" - permissions: - checks: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -90,7 +88,8 @@ jobs: - name: Qualityannotate if: success() && steps.findPr.outputs.number env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} + # ${{ secrets.TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PULL_REQUEST: ${{ steps.findPr.outputs.pr }} GITHUB_PROJECT: balrok/qualityannotate # ${{env.GITHUB_ACTION_REPOSITORY}} SONARQUBE_PROJECT: quyt_qualityannotate diff --git a/src/main/java/org/qualityannotate/api/coderepository/CodeRepository.java b/src/main/java/org/qualityannotate/api/coderepository/CodeRepository.java index cfc7af5..e55246f 100644 --- a/src/main/java/org/qualityannotate/api/coderepository/CodeRepository.java +++ b/src/main/java/org/qualityannotate/api/coderepository/CodeRepository.java @@ -2,8 +2,11 @@ import java.util.List; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; + public interface CodeRepository { - void createOrUpdateAnnotations(Comment globalComment, List fileComments) throws Exception; + void createOrUpdateAnnotations(Comment globalComment, List fileComments, + MetricsAndIssues metricsAndIssues) throws Exception; String printConfigWithoutSecrets(); } diff --git a/src/main/java/org/qualityannotate/api/coderepository/CodeStructuredExecutor.java b/src/main/java/org/qualityannotate/api/coderepository/CodeStructuredExecutor.java new file mode 100644 index 0000000..da26cd3 --- /dev/null +++ b/src/main/java/org/qualityannotate/api/coderepository/CodeStructuredExecutor.java @@ -0,0 +1,14 @@ +package org.qualityannotate.api.coderepository; + +import org.qualityannotate.api.coderepository.api.CodeStructuredApi; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; + +public final class CodeStructuredExecutor { + private CodeStructuredExecutor() { + + } + + public static void run(CodeStructuredApi codeStructuredApi, MetricsAndIssues metricsAndIssues) throws Exception { + codeStructuredApi.update(metricsAndIssues); + } +} diff --git a/src/main/java/org/qualityannotate/api/coderepository/AbstractCodeRepository.java b/src/main/java/org/qualityannotate/api/coderepository/CodeTextExecutor.java similarity index 67% rename from src/main/java/org/qualityannotate/api/coderepository/AbstractCodeRepository.java rename to src/main/java/org/qualityannotate/api/coderepository/CodeTextExecutor.java index 984ec9c..17c2af7 100644 --- a/src/main/java/org/qualityannotate/api/coderepository/AbstractCodeRepository.java +++ b/src/main/java/org/qualityannotate/api/coderepository/CodeTextExecutor.java @@ -4,41 +4,25 @@ import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; -import org.qualityannotate.api.coderepository.api.CodeApi; -import org.qualityannotate.api.coderepository.api.CodeFileComment; -import org.qualityannotate.api.coderepository.api.CodeMainComment; +import org.qualityannotate.api.coderepository.api.CodeStructuredApi; +import org.qualityannotate.api.coderepository.api.CodeTextApi; +import org.qualityannotate.api.coderepository.api.CodeTextFileComment; +import org.qualityannotate.api.coderepository.api.CodeTextMainComment; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; -public abstract class AbstractCodeRepository implements CodeRepository { +public final class CodeTextExecutor { + private CodeTextExecutor() { - protected static Map, String> convertFileCommentsToMap(List fileComments) { - Map, List> fileLineToCommentList = new HashMap<>(); - for (FileComment fileComment : fileComments) { - List comments = fileLineToCommentList - .computeIfAbsent(Pair.of(fileComment.fileName(), fileComment.linenumber()), k -> new ArrayList<>()); - comments.add(fileComment.comment()); - } - return fileLineToCommentList.entrySet() - .stream() - .map(e -> Map.entry(e.getKey(), e.getValue().stream().reduce((c1, c2) -> c1).orElse(Comment.EMPTY))) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().markdown())); } - protected abstract CodeApi getCodeApi(); - - @Override - public void createOrUpdateAnnotations(Comment globalComment, List fileComments) { - CodeApi codeApi = getCodeApi(); - Optional mainComment = Optional.empty(); + public static void run(CodeTextApi codeTextApi, Comment globalComment, List fileComments) + throws Exception { + Optional mainComment = Optional.empty(); try { - mainComment = codeApi.getMainComment(); + mainComment = codeTextApi.getMainComment(); } catch (IOException e) { Log.warn("Could not retrieve main comment"); } @@ -50,20 +34,20 @@ public void createOrUpdateAnnotations(Comment globalComment, List f } }, () -> { try { - codeApi.createMainComment(globalComment.markdown()); + codeTextApi.createMainComment(globalComment.markdown()); } catch (IOException e) { Log.warn("Could not create main comment", e); } }); Map, String> fileLineToComment = convertFileCommentsToMap(fileComments); - List githubFileComments = Collections.emptyList(); + List githubFileComments = Collections.emptyList(); try { - githubFileComments = codeApi.listFileComments(); + githubFileComments = codeTextApi.listFileComments(); } catch (IOException e) { Log.warn("Could not retrieve comments"); } - for (CodeFileComment review : githubFileComments) { + for (CodeTextFileComment review : githubFileComments) { String comment = fileLineToComment.get(review.getFileLine()); Log.info("Found existing file comment"); if (comment != null) { @@ -90,11 +74,29 @@ public void createOrUpdateAnnotations(Comment globalComment, List f for (Map.Entry, String> fileLineCommentEntry : fileLineToComment.entrySet()) { try { Log.infof("Creating %s", fileLineCommentEntry.getKey()); - codeApi.createFileComment(fileLineCommentEntry.getKey().getLeft(), + codeTextApi.createFileComment(fileLineCommentEntry.getKey().getLeft(), fileLineCommentEntry.getKey().getRight(), fileLineCommentEntry.getValue()); } catch (IOException e) { Log.warn("Could not create comment", e); } } } + + private static Map, String> convertFileCommentsToMap(List fileComments) { + Map, List> fileLineToCommentList = new HashMap<>(); + for (FileComment fileComment : fileComments) { + List comments = fileLineToCommentList + .computeIfAbsent(Pair.of(fileComment.fileName(), fileComment.linenumber()), k -> new ArrayList<>()); + comments.add(fileComment.comment()); + } + return fileLineToCommentList.entrySet() + .stream() + .map(e -> Map.entry(e.getKey(), e.getValue().stream().reduce((c1, c2) -> c1).orElse(Comment.EMPTY))) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().markdown())); + } + + private static void runCodeStructuredApi(CodeStructuredApi codeStructuredApi, MetricsAndIssues metricsAndIssues) + throws Exception { + codeStructuredApi.update(metricsAndIssues); + } } diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeApi.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeApi.java deleted file mode 100644 index 7e7337d..0000000 --- a/src/main/java/org/qualityannotate/api/coderepository/api/CodeApi.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.qualityannotate.api.coderepository.api; - -import java.io.IOException; -import java.util.List; -import java.util.Optional; - -public interface CodeApi { - Optional getMainComment() throws IOException; - - void createMainComment(String comment) throws IOException; - - List listFileComments() throws IOException; - - void createFileComment(String file, Integer line, String comment) throws IOException; -} diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeStructuredApi.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeStructuredApi.java new file mode 100644 index 0000000..3c3dccd --- /dev/null +++ b/src/main/java/org/qualityannotate/api/coderepository/api/CodeStructuredApi.java @@ -0,0 +1,14 @@ +package org.qualityannotate.api.coderepository.api; + +import java.io.IOException; + +import org.qualityannotate.api.qualitytool.MetricsAndIssues; + +/** + * Abstraction for the API of Code repositories like Gitlab, github, bitbucket, etc. + * Can be used best for APIs which are structured and know about issues with severity. So github-checks + * api and bitbucket-report api are well suited. + */ +public interface CodeStructuredApi { + void update(MetricsAndIssues metricsAndIssues) throws IOException; +} diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextApi.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextApi.java new file mode 100644 index 0000000..f5bed86 --- /dev/null +++ b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextApi.java @@ -0,0 +1,20 @@ +package org.qualityannotate.api.coderepository.api; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +/** + * Abstraction for the API of Code repositories like Gitlab, github, bitbucket, etc. + * Can be used best for APIs which are only text-based. So github-checks api and bitbucket-report api should use a + * different interface. + */ +public interface CodeTextApi { + Optional getMainComment() throws IOException; + + void createMainComment(String comment) throws IOException; + + List listFileComments() throws IOException; + + void createFileComment(String file, Integer line, String comment) throws IOException; +} diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeFileComment.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextFileComment.java similarity index 88% rename from src/main/java/org/qualityannotate/api/coderepository/api/CodeFileComment.java rename to src/main/java/org/qualityannotate/api/coderepository/api/CodeTextFileComment.java index 13f2fc1..3abcc98 100644 --- a/src/main/java/org/qualityannotate/api/coderepository/api/CodeFileComment.java +++ b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextFileComment.java @@ -4,7 +4,7 @@ import java.io.IOException; -public interface CodeFileComment { +public interface CodeTextFileComment { void update(String comment) throws IOException; void delete() throws IOException; diff --git a/src/main/java/org/qualityannotate/api/coderepository/api/CodeMainComment.java b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextMainComment.java similarity index 77% rename from src/main/java/org/qualityannotate/api/coderepository/api/CodeMainComment.java rename to src/main/java/org/qualityannotate/api/coderepository/api/CodeTextMainComment.java index 960945c..c553e5e 100644 --- a/src/main/java/org/qualityannotate/api/coderepository/api/CodeMainComment.java +++ b/src/main/java/org/qualityannotate/api/coderepository/api/CodeTextMainComment.java @@ -2,6 +2,6 @@ import java.io.IOException; -public interface CodeMainComment { +public interface CodeTextMainComment { void update(String comment) throws IOException; } diff --git a/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java b/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java index 084d15f..eec570b 100644 --- a/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java +++ b/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java @@ -2,5 +2,5 @@ import java.util.Map; -public record GlobalMetrics(Map metrics) { +public record GlobalMetrics(Map metrics, String url) { } diff --git a/src/main/java/org/qualityannotate/api/qualitytool/Issue.java b/src/main/java/org/qualityannotate/api/qualitytool/Issue.java index 170233b..bc5e69c 100644 --- a/src/main/java/org/qualityannotate/api/qualitytool/Issue.java +++ b/src/main/java/org/qualityannotate/api/qualitytool/Issue.java @@ -2,5 +2,10 @@ import jakarta.annotation.Nullable; -public record Issue(String fileName, Integer lineNumber, String comment, String severity, @Nullable String urlToIssue) { +public record Issue(String fileName, Integer lineNumber, String comment, String severity, String severityIcon, + Severity severityEnum, @Nullable String urlToIssue) { + + public enum Severity { + LOW, MEDIUM, HIGH + } } diff --git a/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java b/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java index fa581a8..f8c1602 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java +++ b/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java @@ -2,28 +2,38 @@ import jakarta.enterprise.context.ApplicationScoped; -import org.qualityannotate.api.coderepository.AbstractCodeRepository; +import java.util.List; + import org.qualityannotate.api.coderepository.CodeRepository; -import org.qualityannotate.api.coderepository.api.CodeApi; -import org.qualityannotate.coderepo.github.client.GithubApi; +import org.qualityannotate.api.coderepository.CodeStructuredExecutor; +import org.qualityannotate.api.coderepository.Comment; +import org.qualityannotate.api.coderepository.FileComment; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; +import org.qualityannotate.coderepo.github.client.GithubStructuredApi; +import org.qualityannotate.coderepo.github.client.GithubTextApi; @ApplicationScoped -public class GithubCodeRepository extends AbstractCodeRepository implements CodeRepository { +public class GithubCodeRepository implements CodeRepository { private final GithubConfig config; - private final GithubApi githubApi; + private final GithubTextApi githubTextsApi; + private final GithubStructuredApi githubStructuredApi; public GithubCodeRepository(GithubConfig config) { this.config = config; - this.githubApi = new GithubApi(config.token(), config.project(), config.pullRequest()); + this.githubTextsApi = new GithubTextApi(config); + this.githubStructuredApi = new GithubStructuredApi(config); } @Override - public String printConfigWithoutSecrets() { - return config.printWithoutSecrets(); + public void createOrUpdateAnnotations(Comment globalComment, List fileComments, + MetricsAndIssues metricsAndIssues) throws Exception { + CodeStructuredExecutor.run(githubStructuredApi, metricsAndIssues); + // CodeTextExecutor.run(githubTextsApi, globalComment, fileComments); } @Override - protected CodeApi getCodeApi() { - return githubApi; + public String printConfigWithoutSecrets() { + return config.printWithoutSecrets(); } + } diff --git a/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java b/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java new file mode 100644 index 0000000..31140b9 --- /dev/null +++ b/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java @@ -0,0 +1,89 @@ +package org.qualityannotate.coderepo.github.client; + +import org.kohsuke.github.*; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import org.qualityannotate.api.coderepository.api.CodeStructuredApi; +import org.qualityannotate.api.qualitytool.Issue; +import org.qualityannotate.api.qualitytool.MetricsAndIssues; +import org.qualityannotate.coderepo.github.GithubConfig; +import org.qualityannotate.core.CommentProcessor; + +public class GithubStructuredApi implements CodeStructuredApi { + public static final String NAME = "qualityannotate"; + private final GitHubBuilder gitHubBuilder; + private final String project; + private final int pullRequestId; + /** + * Guaranteed to be not null by {@link #init()} + */ + private GitHub github = null; + + public GithubStructuredApi(GithubConfig config) { + gitHubBuilder = new GitHubBuilder().withOAuthToken(config.token()); + this.project = config.project(); + this.pullRequestId = config.pullRequest(); + } + + private static Optional findCheckRun(GHRepository repository, GHPullRequest pullRequest) + throws IOException { + PagedIterator iterator = repository.getCheckRuns(pullRequest.getHead().getSha()).iterator(); + while (iterator.hasNext()) { + GHCheckRun next = iterator.next(); + if (next.getName().equals(NAME)) { + return Optional.of(next); + } + } + return Optional.empty(); + } + + private void init() throws IOException { + if (github == null) { + github = gitHubBuilder.build(); + // github.checkApiUrlValidity(); + } + } + + @Override + public void update(MetricsAndIssues metricsAndIssues) throws IOException { + init(); + GHRepository repository = github.getRepository(project); + GHPullRequest pullRequest = repository.getPullRequest(pullRequestId); + Optional checkRunOpt = findCheckRun(repository, pullRequest); + if (checkRunOpt.isPresent()) { + GHCheckRun checkRun = checkRunOpt.get(); + GHCheckRunBuilder checkRunBuilder = checkRun.update(); + updateCheckRun(checkRunBuilder, metricsAndIssues); + } else { + GHCheckRunBuilder checkRunBuilder = repository.createCheckRun(NAME, pullRequest.getHead().getSha()); + updateCheckRun(checkRunBuilder, metricsAndIssues); + } + } + + private void updateCheckRun(GHCheckRunBuilder checkRunBuilder, MetricsAndIssues metricsAndIssues) + throws IOException { + checkRunBuilder.withDetailsURL(metricsAndIssues.globalMetrics().url()); + checkRunBuilder.withStatus(GHCheckRun.Status.COMPLETED); + checkRunBuilder.withConclusion(GHCheckRun.Conclusion.NEUTRAL); + GHCheckRunBuilder.Output output = new GHCheckRunBuilder.Output("Result", + CommentProcessor.createGlobalComment(metricsAndIssues.globalMetrics()).markdown()); + List issues = metricsAndIssues.issues(); + for (Issue issue : issues) { + output.add(new GHCheckRunBuilder.Annotation(issue.fileName(), issue.lineNumber(), + mapSeverity(issue.severityEnum()), CommentProcessor.createComment(issue).comment().markdown())); + } + checkRunBuilder.add(output); + checkRunBuilder.create(); + } + + private GHCheckRun.AnnotationLevel mapSeverity(Issue.Severity severity) { + return switch (severity) { + case LOW -> GHCheckRun.AnnotationLevel.NOTICE; + case MEDIUM -> GHCheckRun.AnnotationLevel.WARNING; + case HIGH -> GHCheckRun.AnnotationLevel.FAILURE; + }; + } +} diff --git a/src/main/java/org/qualityannotate/coderepo/github/client/GithubApi.java b/src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java similarity index 75% rename from src/main/java/org/qualityannotate/coderepo/github/client/GithubApi.java rename to src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java index 759087d..f7859f7 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/client/GithubApi.java +++ b/src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java @@ -2,24 +2,19 @@ import io.quarkus.logging.Log; import org.apache.commons.lang3.tuple.Pair; -import org.kohsuke.github.GHIssueComment; -import org.kohsuke.github.GHPullRequest; -import org.kohsuke.github.GHPullRequestReviewComment; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GitHub; -import org.kohsuke.github.GitHubBuilder; -import org.kohsuke.github.PagedIterator; +import org.kohsuke.github.*; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import org.qualityannotate.api.coderepository.api.CodeApi; -import org.qualityannotate.api.coderepository.api.CodeFileComment; -import org.qualityannotate.api.coderepository.api.CodeMainComment; +import org.qualityannotate.api.coderepository.api.CodeTextApi; +import org.qualityannotate.api.coderepository.api.CodeTextFileComment; +import org.qualityannotate.api.coderepository.api.CodeTextMainComment; +import org.qualityannotate.coderepo.github.GithubConfig; -public class GithubApi implements CodeApi { +public class GithubTextApi implements CodeTextApi { private final GitHubBuilder gitHubBuilder; private final String project; private final int pullRequestId; @@ -33,10 +28,10 @@ public class GithubApi implements CodeApi { */ private long userId = 0L; - public GithubApi(String token, String project, int pullRequestId) { - gitHubBuilder = new GitHubBuilder().withOAuthToken(token); - this.project = project; - this.pullRequestId = pullRequestId; + public GithubTextApi(GithubConfig config) { + gitHubBuilder = new GitHubBuilder().withOAuthToken(config.token()); + this.project = config.project(); + this.pullRequestId = config.pullRequest(); } private void init() throws IOException { @@ -57,8 +52,7 @@ private GHPullRequest getPR() throws IOException { } @Override - public Optional getMainComment() throws IOException { - init(); + public Optional getMainComment() throws IOException { GHPullRequest pullRequest = getPR(); PagedIterator commentIterator = pullRequest.listComments().iterator(); while (commentIterator.hasNext()) { @@ -75,21 +69,19 @@ public Optional getMainComment() throws IOException { @Override public void createMainComment(String comment) throws IOException { - init(); GHPullRequest pullRequest = getPR(); System.out.println("Creating new global comment"); pullRequest.comment(comment); } @Override - public List listFileComments() throws IOException { - init(); + public List listFileComments() throws IOException { Log.info("Start creating file comments"); GHPullRequest pullRequest = getPR(); - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (GHPullRequestReviewComment review : pullRequest.listReviewComments()) { if (review.getUser().getId() == userId) { - result.add(new CodeFileComment() { + result.add(new CodeTextFileComment() { @Override public void update(String comment) throws IOException { review.update(comment); @@ -117,7 +109,6 @@ public String getComment() { @Override public void createFileComment(String file, Integer line, String comment) throws IOException { - init(); GHPullRequest pullRequest = getPR(); String commitHash = pullRequest.getHead().getSha(); line = (line == null || line < 1) ? 1 : line; diff --git a/src/main/java/org/qualityannotate/core/CommentProcessor.java b/src/main/java/org/qualityannotate/core/CommentProcessor.java index 7d9911b..f76893e 100644 --- a/src/main/java/org/qualityannotate/core/CommentProcessor.java +++ b/src/main/java/org/qualityannotate/core/CommentProcessor.java @@ -7,19 +7,18 @@ import org.qualityannotate.api.coderepository.FileComment; import org.qualityannotate.api.qualitytool.GlobalMetrics; import org.qualityannotate.api.qualitytool.Issue; -import org.qualityannotate.api.qualitytool.QualityTool; public class CommentProcessor { private CommentProcessor() { } - public static Comment createGlobalComment(GlobalMetrics globalMetrics, QualityTool qualityTool) { + public static Comment createGlobalComment(GlobalMetrics globalMetrics) { String markdown = String.format(""" Code Quality Report for [SonarQube](%s) | Name | Value | |------|-------| %s - """, qualityTool.getUrl(), + """, globalMetrics.url(), globalMetrics.metrics() .entrySet() .stream() @@ -28,14 +27,14 @@ public static Comment createGlobalComment(GlobalMetrics globalMetrics, QualityTo return new Comment(markdown, markdown, "TODO-html"); } - public static List createFileComments(List issues, QualityTool qualityTool) { - return issues.stream().map(issue -> createComment(issue, qualityTool)).toList(); + public static List createFileComments(List issues) { + return issues.stream().map(issue -> createComment(issue)).toList(); } - private static FileComment createComment(Issue issue, QualityTool qualityTool) { + public static FileComment createComment(Issue issue) { String markdown = String.format(""" %s %s%s - """, qualityTool.getSeverityIcon(issue.severity()), issue.comment(), + """, issue.severityIcon(), issue.comment(), (issue.urlToIssue() == null ? "" : " [details](" + issue.urlToIssue() + ")")); Comment comment = new Comment(markdown, markdown, "TODO-html"); return new FileComment(issue.fileName(), issue.lineNumber(), comment); diff --git a/src/main/java/org/qualityannotate/core/MainCommand.java b/src/main/java/org/qualityannotate/core/MainCommand.java index d2abc9e..4101382 100644 --- a/src/main/java/org/qualityannotate/core/MainCommand.java +++ b/src/main/java/org/qualityannotate/core/MainCommand.java @@ -35,17 +35,16 @@ public class MainCommand implements Runnable { @Parameters(paramLabel = "", defaultValue = GithubConfig.NAME, description = "To which code-repository you want to upload the data. Possible Values: ${COMPLETION-CANDIDATES}; Default: ${DEFAULT-VALUE}", completionCandidates = CodeRepoProvider.CodeRepositoryCandidates.class) String codeRepositoryParam; - private static void updateAnnotations(QualityTool qualityTool, CodeRepository codeRepository, - MetricsAndIssues metricsAndIssues) { - Comment globalComment = CommentProcessor.createGlobalComment(metricsAndIssues.globalMetrics(), qualityTool); - List fileComments = CommentProcessor.createFileComments(metricsAndIssues.issues(), qualityTool); + private static void updateAnnotations(CodeRepository codeRepository, MetricsAndIssues metricsAndIssues) { + Comment globalComment = CommentProcessor.createGlobalComment(metricsAndIssues.globalMetrics()); + List fileComments = CommentProcessor.createFileComments(metricsAndIssues.issues()); Log.infof("Global comment:\n%s", globalComment.text()); Log.infof("Issues:\n%s", fileComments.stream() .map(c -> String.format("%s:%d\n%s", c.fileName(), c.linenumber(), c.comment().text())) .collect(Collectors.joining("\n\n"))); try { - codeRepository.createOrUpdateAnnotations(globalComment, fileComments); + codeRepository.createOrUpdateAnnotations(globalComment, fileComments, metricsAndIssues); } catch (Exception e) { Log.error("Updating the code-repository failed", e); System.exit(1); @@ -72,6 +71,6 @@ public void run() { Log.infof("Code-Repository tool configured with\n%s", codeRepository.printConfigWithoutSecrets()); MetricsAndIssues metricsAndIssues = getMetricsAndIssues(qualityTool); Log.info(metricsAndIssues); - updateAnnotations(qualityTool, codeRepository, metricsAndIssues); + updateAnnotations(codeRepository, metricsAndIssues); } } diff --git a/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java b/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java index f5f4f1c..de6e4aa 100644 --- a/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java +++ b/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java @@ -5,24 +5,14 @@ import jakarta.inject.Inject; import java.net.URI; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; +import java.util.*; import org.qualityannotate.api.qualitytool.GlobalMetrics; import org.qualityannotate.api.qualitytool.Issue; import org.qualityannotate.api.qualitytool.MetricsAndIssues; import org.qualityannotate.api.qualitytool.QualityTool; import org.qualityannotate.core.rest.BasicAuthRequestFilter; -import org.qualityannotate.quality.sonarqube.client.ComponentMeasures; -import org.qualityannotate.quality.sonarqube.client.IssueSearch; -import org.qualityannotate.quality.sonarqube.client.Measure; -import org.qualityannotate.quality.sonarqube.client.Metric; -import org.qualityannotate.quality.sonarqube.client.SonarqubeApiClient; -import org.qualityannotate.quality.sonarqube.client.SqIssue; +import org.qualityannotate.quality.sonarqube.client.*; @ApplicationScoped public class SonarqubeQualityTool implements QualityTool { @@ -45,6 +35,7 @@ List getIssues() { for (SqIssue sqIssue : issuesSearch.issues()) { issues.add(new Issue(sqIssue.getPath(config.project()), sqIssue.textRange().startLine(), sqIssue.getQualityType().map(q -> q + ": ").orElse("") + sqIssue.message(), sqIssue.getSeverity(), + getSeverityIcon(sqIssue.getSeverity()), getSeverityEnum(sqIssue.getSeverity()), getIssueUrl(sqIssue.rule()))); } return issues; @@ -79,7 +70,7 @@ GlobalMetrics getGlobalMetrics() { metrics.put(metric.getName(), value); } } - return new GlobalMetrics(metrics); + return new GlobalMetrics(metrics, getUrl()); } private String getIssueUrl(String rule) { @@ -111,6 +102,15 @@ public String getSeverityIcon(String severity) { }; } + public Issue.Severity getSeverityEnum(String severity) { + return switch (severity.toLowerCase(Locale.ENGLISH)) { + case "low" -> Issue.Severity.LOW; + case "medium" -> Issue.Severity.MEDIUM; + // case "high" -> Issue.Severity.HIGH; + default -> Issue.Severity.HIGH; + }; + } + @Override public String printConfigWithoutSecrets() { return config.printWithoutSecrets(); diff --git a/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java b/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java index 5e6c03c..87e93e3 100644 --- a/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java +++ b/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java @@ -1,9 +1,6 @@ package org.qualityannotate.quality.sonarqube; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.okJson; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathTemplate; +import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.junit.jupiter.api.Assertions.assertEquals; import com.github.jknack.handlebars.internal.Files; @@ -56,8 +53,10 @@ void testGetGlobalMetrics() { .withQueryParam("metricKeys", equalTo("test-metrics-1,test-metrics-2")) .withQueryParam("additionalFields", equalTo("metrics")) .willReturn(okJson(content))); - assertEquals(new GlobalMetrics(Map.of("New issues", "25", "Lines of code", "114", "Complexity", "12")), - cut.getGlobalMetrics()); + GlobalMetrics globalMetrics = cut.getGlobalMetrics(); + assertEquals(Map.of("Complexity", "12", "New issues", "25", "Lines of code", "114"), globalMetrics.metrics()); + assertEquals("http://localhost:42345/summary/new_code?id=Test_Project&pullRequest=Test_Pr".replaceAll(":[0-9]+", + ":123"), globalMetrics.url().replaceAll(":[0-9]+", ":123")); } @Test @@ -77,8 +76,8 @@ void testGetIssues() { .willReturn(okJson(content))); assertEquals( List.of(new Issue("om.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", 2, - "Remove this unused private \"getKee\" method.", "MAJOR", - "https://sonarcloud" + ".io/organizations/quyt/rules?open=java:S1144&rule_key=java:S1144")), + "Remove this unused private \"getKee\" method.", "MAJOR", "MAJOR", Issue.Severity.HIGH, + "https://sonarcloud.io/organizations/quyt/rules?open=java:S1144&rule_key=java:S1144")), cut.getIssues()); } From fdc24e573b212df7adddc63dfe695f6309eabc8a Mon Sep 17 00:00:00 2001 From: Carl Mai Date: Sat, 28 Oct 2023 20:36:35 +0200 Subject: [PATCH 2/2] feat: check-api use text-formatting and upload an image --- .github/workflows/build.yml | 2 +- .../api/qualitytool/GlobalMetrics.java | 8 +++- .../api/qualitytool/Issue.java | 17 +++++-- .../api/qualitytool/QualityTool.java | 6 --- .../coderepo/github/GithubCodeRepository.java | 22 +++++---- .../coderepo/github/GithubConfig.java | 11 ++++- .../github/client/GithubStructuredApi.java | 43 +++++++++++------ .../coderepo/github/client/GithubTextApi.java | 2 + .../core/CommentProcessor.java | 12 +++-- .../sonarqube/SonarqubeQualityTool.java | 47 ++++++++++--------- .../sonarqube/SonarqubeQualityToolTest.java | 9 ++-- 11 files changed, 113 insertions(+), 66 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 452cdd4..df59df1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,7 +40,7 @@ jobs: java-version: '17' cache: 'gradle' - name: Cache SonarQube packages - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar diff --git a/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java b/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java index eec570b..c15240c 100644 --- a/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java +++ b/src/main/java/org/qualityannotate/api/qualitytool/GlobalMetrics.java @@ -1,6 +1,12 @@ package org.qualityannotate.api.qualitytool; +import jakarta.annotation.Nullable; + import java.util.Map; +import java.util.Optional; -public record GlobalMetrics(Map metrics, String url) { +public record GlobalMetrics(Map metrics, String url, @Nullable String statusUrl) { + public Optional getStatusUrl() { + return Optional.ofNullable(statusUrl); + } } diff --git a/src/main/java/org/qualityannotate/api/qualitytool/Issue.java b/src/main/java/org/qualityannotate/api/qualitytool/Issue.java index bc5e69c..28a2679 100644 --- a/src/main/java/org/qualityannotate/api/qualitytool/Issue.java +++ b/src/main/java/org/qualityannotate/api/qualitytool/Issue.java @@ -1,11 +1,22 @@ package org.qualityannotate.api.qualitytool; import jakarta.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Getter; -public record Issue(String fileName, Integer lineNumber, String comment, String severity, String severityIcon, - Severity severityEnum, @Nullable String urlToIssue) { +public record Issue(String fileName, Integer startLine, @Nullable Integer endLine, @Nullable Integer startColumn, + @Nullable Integer endColumn, String comment, String severity, Severity severityEnum, + @Nullable String urlToIssue) { + public Integer lineNumber() { + return startLine; + } + + @AllArgsConstructor + @Getter public enum Severity { - LOW, MEDIUM, HIGH + LOW("ℹ\uFE0F"), MEDIUM("⚠\uFE0F"), HIGH("\uD83D\uDED1"); + + private final String unicodeIcon; } } diff --git a/src/main/java/org/qualityannotate/api/qualitytool/QualityTool.java b/src/main/java/org/qualityannotate/api/qualitytool/QualityTool.java index 3b922bf..c066f28 100644 --- a/src/main/java/org/qualityannotate/api/qualitytool/QualityTool.java +++ b/src/main/java/org/qualityannotate/api/qualitytool/QualityTool.java @@ -3,11 +3,5 @@ public interface QualityTool { MetricsAndIssues getMetricsAndIssues() throws Exception; - String getSeverityReadable(String severity); - - String getSeverityIcon(String severity); - String printConfigWithoutSecrets(); - - String getUrl(); } diff --git a/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java b/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java index f8c1602..73ec26d 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java +++ b/src/main/java/org/qualityannotate/coderepo/github/GithubCodeRepository.java @@ -1,11 +1,14 @@ package org.qualityannotate.coderepo.github; +import io.quarkus.logging.Log; import jakarta.enterprise.context.ApplicationScoped; +import lombok.AllArgsConstructor; import java.util.List; import org.qualityannotate.api.coderepository.CodeRepository; import org.qualityannotate.api.coderepository.CodeStructuredExecutor; +import org.qualityannotate.api.coderepository.CodeTextExecutor; import org.qualityannotate.api.coderepository.Comment; import org.qualityannotate.api.coderepository.FileComment; import org.qualityannotate.api.qualitytool.MetricsAndIssues; @@ -13,27 +16,26 @@ import org.qualityannotate.coderepo.github.client.GithubTextApi; @ApplicationScoped +@AllArgsConstructor public class GithubCodeRepository implements CodeRepository { private final GithubConfig config; - private final GithubTextApi githubTextsApi; + private final GithubTextApi githubTextApi; private final GithubStructuredApi githubStructuredApi; - public GithubCodeRepository(GithubConfig config) { - this.config = config; - this.githubTextsApi = new GithubTextApi(config); - this.githubStructuredApi = new GithubStructuredApi(config); - } - @Override public void createOrUpdateAnnotations(Comment globalComment, List fileComments, MetricsAndIssues metricsAndIssues) throws Exception { - CodeStructuredExecutor.run(githubStructuredApi, metricsAndIssues); - // CodeTextExecutor.run(githubTextsApi, globalComment, fileComments); + if (config.useChecks()) { + Log.info("Using the checks api"); + CodeStructuredExecutor.run(githubStructuredApi, metricsAndIssues); + } else { + Log.info("Using the comment-based api"); + CodeTextExecutor.run(githubTextApi, globalComment, fileComments); + } } @Override public String printConfigWithoutSecrets() { return config.printWithoutSecrets(); } - } diff --git a/src/main/java/org/qualityannotate/coderepo/github/GithubConfig.java b/src/main/java/org/qualityannotate/coderepo/github/GithubConfig.java index 68be59e..2d6ca86 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/GithubConfig.java +++ b/src/main/java/org/qualityannotate/coderepo/github/GithubConfig.java @@ -1,6 +1,7 @@ package org.qualityannotate.coderepo.github; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; import io.smallrye.config.WithName; @ConfigMapping(prefix = GithubConfig.NAME) @@ -16,10 +17,18 @@ public interface GithubConfig { @WithName("pull_request") Integer pullRequest(); + /** + * when true will use the checks-api, otherwise it will write comments + */ + @WithName("use_checks") + @WithDefault("true") + Boolean useChecks(); + default String printWithoutSecrets() { return String.format(""" project: %s pull_request: %s - """, project(), pullRequest()); + use_checks: %s + """, project(), pullRequest(), useChecks()); } } diff --git a/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java b/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java index 31140b9..7bb0bdb 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java +++ b/src/main/java/org/qualityannotate/coderepo/github/client/GithubStructuredApi.java @@ -1,8 +1,17 @@ package org.qualityannotate.coderepo.github.client; -import org.kohsuke.github.*; +import jakarta.enterprise.context.ApplicationScoped; +import org.kohsuke.github.GHCheckRun; +import org.kohsuke.github.GHCheckRunBuilder; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.kohsuke.github.PagedIterator; import java.io.IOException; +import java.sql.Date; +import java.time.Instant; import java.util.List; import java.util.Optional; @@ -12,15 +21,17 @@ import org.qualityannotate.coderepo.github.GithubConfig; import org.qualityannotate.core.CommentProcessor; +/** + * This is using the Github checks api. + * It seems like it can't be executed locally but only in a github-actions job. + */ +@ApplicationScoped public class GithubStructuredApi implements CodeStructuredApi { public static final String NAME = "qualityannotate"; + private final GitHubBuilder gitHubBuilder; private final String project; private final int pullRequestId; - /** - * Guaranteed to be not null by {@link #init()} - */ - private GitHub github = null; public GithubStructuredApi(GithubConfig config) { gitHubBuilder = new GitHubBuilder().withOAuthToken(config.token()); @@ -40,16 +51,9 @@ private static Optional findCheckRun(GHRepository repository, GHPull return Optional.empty(); } - private void init() throws IOException { - if (github == null) { - github = gitHubBuilder.build(); - // github.checkApiUrlValidity(); - } - } - @Override public void update(MetricsAndIssues metricsAndIssues) throws IOException { - init(); + GitHub github = gitHubBuilder.build(); GHRepository repository = github.getRepository(project); GHPullRequest pullRequest = repository.getPullRequest(pullRequestId); Optional checkRunOpt = findCheckRun(repository, pullRequest); @@ -68,13 +72,22 @@ private void updateCheckRun(GHCheckRunBuilder checkRunBuilder, MetricsAndIssues checkRunBuilder.withDetailsURL(metricsAndIssues.globalMetrics().url()); checkRunBuilder.withStatus(GHCheckRun.Status.COMPLETED); checkRunBuilder.withConclusion(GHCheckRun.Conclusion.NEUTRAL); + checkRunBuilder.withCompletedAt(Date.from(Instant.now())); + GHCheckRunBuilder.Output output = new GHCheckRunBuilder.Output("Result", CommentProcessor.createGlobalComment(metricsAndIssues.globalMetrics()).markdown()); List issues = metricsAndIssues.issues(); for (Issue issue : issues) { - output.add(new GHCheckRunBuilder.Annotation(issue.fileName(), issue.lineNumber(), - mapSeverity(issue.severityEnum()), CommentProcessor.createComment(issue).comment().markdown())); + GHCheckRunBuilder.Annotation annotation = new GHCheckRunBuilder.Annotation(issue.fileName(), + issue.startLine(), Optional.ofNullable(issue.endLine()).orElse(issue.startLine()), + mapSeverity(issue.severityEnum()), CommentProcessor.createComment(issue).comment().text()); + Optional.ofNullable(issue.startColumn()).ifPresent(annotation::withStartColumn); + Optional.ofNullable(issue.endColumn()).ifPresent(annotation::withEndColumn); + output.add(annotation); } + metricsAndIssues.globalMetrics() + .getStatusUrl() + .ifPresent(u -> output.add(new GHCheckRunBuilder.Image("Status", u))); checkRunBuilder.add(output); checkRunBuilder.create(); } diff --git a/src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java b/src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java index f7859f7..c0cce8b 100644 --- a/src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java +++ b/src/main/java/org/qualityannotate/coderepo/github/client/GithubTextApi.java @@ -1,6 +1,7 @@ package org.qualityannotate.coderepo.github.client; import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; import org.apache.commons.lang3.tuple.Pair; import org.kohsuke.github.*; @@ -14,6 +15,7 @@ import org.qualityannotate.api.coderepository.api.CodeTextMainComment; import org.qualityannotate.coderepo.github.GithubConfig; +@ApplicationScoped public class GithubTextApi implements CodeTextApi { private final GitHubBuilder gitHubBuilder; private final String project; diff --git a/src/main/java/org/qualityannotate/core/CommentProcessor.java b/src/main/java/org/qualityannotate/core/CommentProcessor.java index f76893e..2c1bd47 100644 --- a/src/main/java/org/qualityannotate/core/CommentProcessor.java +++ b/src/main/java/org/qualityannotate/core/CommentProcessor.java @@ -28,15 +28,19 @@ public static Comment createGlobalComment(GlobalMetrics globalMetrics) { } public static List createFileComments(List issues) { - return issues.stream().map(issue -> createComment(issue)).toList(); + return issues.stream().map(CommentProcessor::createComment).toList(); } public static FileComment createComment(Issue issue) { String markdown = String.format(""" - %s %s%s - """, issue.severityIcon(), issue.comment(), + %s%s%s + """, issue.severityEnum().getUnicodeIcon(), issue.comment(), (issue.urlToIssue() == null ? "" : " [details](" + issue.urlToIssue() + ")")); - Comment comment = new Comment(markdown, markdown, "TODO-html"); + String text = String.format(""" + %s%s%s + """, issue.severityEnum().getUnicodeIcon(), issue.comment(), + (issue.urlToIssue() == null ? "" : " " + issue.urlToIssue())); + Comment comment = new Comment(text, markdown, "TODO-html"); return new FileComment(issue.fileName(), issue.lineNumber(), comment); } } diff --git a/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java b/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java index de6e4aa..7a92669 100644 --- a/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java +++ b/src/main/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityTool.java @@ -5,14 +5,24 @@ import jakarta.inject.Inject; import java.net.URI; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; import org.qualityannotate.api.qualitytool.GlobalMetrics; import org.qualityannotate.api.qualitytool.Issue; import org.qualityannotate.api.qualitytool.MetricsAndIssues; import org.qualityannotate.api.qualitytool.QualityTool; import org.qualityannotate.core.rest.BasicAuthRequestFilter; -import org.qualityannotate.quality.sonarqube.client.*; +import org.qualityannotate.quality.sonarqube.client.ComponentMeasures; +import org.qualityannotate.quality.sonarqube.client.IssueSearch; +import org.qualityannotate.quality.sonarqube.client.Measure; +import org.qualityannotate.quality.sonarqube.client.Metric; +import org.qualityannotate.quality.sonarqube.client.SonarqubeApiClient; +import org.qualityannotate.quality.sonarqube.client.SqIssue; @ApplicationScoped public class SonarqubeQualityTool implements QualityTool { @@ -34,9 +44,9 @@ List getIssues() { List issues = new ArrayList<>(); for (SqIssue sqIssue : issuesSearch.issues()) { issues.add(new Issue(sqIssue.getPath(config.project()), sqIssue.textRange().startLine(), + sqIssue.textRange().endLine(), sqIssue.textRange().startOffset(), sqIssue.textRange().startOffset(), sqIssue.getQualityType().map(q -> q + ": ").orElse("") + sqIssue.message(), sqIssue.getSeverity(), - getSeverityIcon(sqIssue.getSeverity()), getSeverityEnum(sqIssue.getSeverity()), - getIssueUrl(sqIssue.rule()))); + getSeverityEnum(sqIssue.getSeverity()), getIssueUrl(sqIssue.rule()))); } return issues; } @@ -70,7 +80,7 @@ GlobalMetrics getGlobalMetrics() { metrics.put(metric.getName(), value); } } - return new GlobalMetrics(metrics, getUrl()); + return new GlobalMetrics(metrics, getUrl(), getStatusUrl()); } private String getIssueUrl(String rule) { @@ -87,21 +97,6 @@ public MetricsAndIssues getMetricsAndIssues() { return new MetricsAndIssues(getGlobalMetrics(), getIssues()); } - @Override - public String getSeverityReadable(String severity) { - return severity; - } - - @Override - public String getSeverityIcon(String severity) { - return switch (severity.toLowerCase(Locale.ENGLISH)) { - case "low" -> "ℹ\uFE0F"; - case "medium" -> "⚠\uFE0F"; - case "high" -> "\uD83D\uDED1"; - default -> severity; - }; - } - public Issue.Severity getSeverityEnum(String severity) { return switch (severity.toLowerCase(Locale.ENGLISH)) { case "low" -> Issue.Severity.LOW; @@ -117,13 +112,21 @@ public String printConfigWithoutSecrets() { } /** - * E.g. https://sonarcloud.io/summary/new_code?id=quyt_qualityannotate&pullRequest=1 + * E.g. example */ - @Override public String getUrl() { return getBaseUrl() + "/summary/new_code?id=" + config.project() + "&pullRequest=" + config.pullRequest(); } + /** + * E.g. + * example + */ + public String getStatusUrl() { + return getBaseUrl() + "/api/project_badges/measure?project=" + config.project() + "&pullRequest=" + + config.pullRequest() + "&metric=alert_status"; + } + private String getBaseUrl() { return config.url().trim().replaceAll("/$", ""); } diff --git a/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java b/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java index 87e93e3..ce4abac 100644 --- a/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java +++ b/src/test/java/org/qualityannotate/quality/sonarqube/SonarqubeQualityToolTest.java @@ -1,6 +1,9 @@ package org.qualityannotate.quality.sonarqube; -import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.okJson; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathTemplate; import static org.junit.jupiter.api.Assertions.assertEquals; import com.github.jknack.handlebars.internal.Files; @@ -75,8 +78,8 @@ void testGetIssues() { .withQueryParam("statuses", equalTo("OPEN,REOPENED,CONFIRMED")) .willReturn(okJson(content))); assertEquals( - List.of(new Issue("om.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", 2, - "Remove this unused private \"getKee\" method.", "MAJOR", "MAJOR", Issue.Severity.HIGH, + List.of(new Issue("om.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", 2, 2, + 0, 0, "Remove this unused private \"getKee\" method.", "MAJOR", Issue.Severity.HIGH, "https://sonarcloud.io/organizations/quyt/rules?open=java:S1144&rule_key=java:S1144")), cut.getIssues()); }