Skip to content

Commit

Permalink
QD-10723 Test summary and comment for azure pipelines
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrei Iurko committed Jan 23, 2025
1 parent 79afc8e commit 1a63fb8
Show file tree
Hide file tree
Showing 12 changed files with 65,965 additions and 769 deletions.
30 changes: 30 additions & 0 deletions common/annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export const FAILURE_LEVEL = 'failure'

Check failure on line 17 in common/annotations.ts

View workflow job for this annotation

GitHub Actions / Qodana for JVM

ESLint

ESLint: Parsing error: The keyword 'export' is reserved
export const WARNING_LEVEL = 'warning'
export const NOTICE_LEVEL = 'notice'

export interface Annotation {
title: string | undefined
path: string
start_line: number
end_line: number
level: 'failure' | 'warning' | 'notice'
message: string
start_column: number | undefined
end_column: number | undefined
}
261 changes: 261 additions & 0 deletions common/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {Coverage, QODANA_OPEN_IN_IDE_NAME, QODANA_REPORT_URL_NAME, VERSION} from "./qodana";

Check failure on line 17 in common/output.ts

View workflow job for this annotation

GitHub Actions / Qodana for JVM

ESLint

ESLint: Parsing error: The keyword 'import' is reserved
import * as fs from "fs";
import {Annotation, FAILURE_LEVEL, NOTICE_LEVEL, WARNING_LEVEL} from "./annotations";

export const QODANA_CHECK_NAME = 'Qodana'
const UNKNOWN_RULE_ID = 'Unknown'
const SUMMARY_TABLE_HEADER = '| Inspection name | Severity | Problems |'
const SUMMARY_TABLE_SEP = '| --- | --- | --- |'
const SUMMARY_MISC = `Contact us at [[email protected]](mailto:[email protected])
- Or via our issue tracker: https://jb.gg/qodana-issue
- Or share your feedback: https://jb.gg/qodana-discussions`
const VIEW_REPORT_OPTIONS = `To be able to view the detailed Qodana report, you can either:
- Register at [Qodana Cloud](https://qodana.cloud/) and [configure the action](https://github.com/jetbrains/qodana-action#qodana-cloud)
- Use [GitHub Code Scanning with Qodana](https://github.com/jetbrains/qodana-action#github-code-scanning)
- Host [Qodana report at GitHub Pages](https://github.com/JetBrains/qodana-action/blob/3a8e25f5caad8d8b01c1435f1ef7b19fe8b039a0/README.md#github-pages)
- Inspect and use \`qodana.sarif.json\` (see [the Qodana SARIF format](https://www.jetbrains.com/help/qodana/qodana-sarif-output.html#Report+structure) for details)
To get \`*.log\` files or any other Qodana artifacts, run the action with \`upload-result\` option set to \`true\`,
so that the action will upload the files as the job artifacts:
\`\`\`yaml
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v${VERSION}
with:
upload-result: true
\`\`\`
`
const SUMMARY_PR_MODE = `💡 Qodana analysis was run in the pull request mode: only the changed files were checked`

interface CloudData {
url?: string
}

interface OpenInIDEData {
cloud?: CloudData
}

export interface LicenseEntry {
name?: string
version?: string
license?: string
}

function wrapToDiffBlock(message: string): string {
return `\`\`\`diff
${message}
\`\`\``
}

export function getCoverageStats(c: Coverage): string {
if (c.totalLines === 0 && c.totalCoveredLines === 0) {
return ''
}

let stats = ''
if (c.totalLines !== 0) {
let conclusion = `${c.totalCoverage}% total lines covered`
if (c.totalCoverage < c.totalCoverageThreshold) {
conclusion = `- ${conclusion}`
} else {
conclusion = `+ ${conclusion}`
}
stats += `${conclusion}
${c.totalLines} lines analyzed, ${c.totalCoveredLines} lines covered`
}

if (c.freshLines !== 0) {
stats += `
! ${c.freshCoverage}% fresh lines covered
${c.freshLines} lines analyzed, ${c.freshCoveredLines} lines covered`
}

return wrapToDiffBlock(
[
`@@ Code coverage @@`,
`${stats}`,
`# Calculated according to the filters of your coverage tool`
].join('\n')
)
}

export function getReportURL(resultsDir: string): string {
let reportUrlFile = `${resultsDir}/${QODANA_OPEN_IN_IDE_NAME}`
if (fs.existsSync(reportUrlFile)) {
const rawData = fs.readFileSync(reportUrlFile, {encoding: 'utf8'})
const data = JSON.parse(rawData) as OpenInIDEData
if (data?.cloud?.url) {
return data.cloud.url
}
} else {
reportUrlFile = `${resultsDir}/${QODANA_REPORT_URL_NAME}`
if (fs.existsSync(reportUrlFile)) {
return fs.readFileSync(reportUrlFile, {encoding: 'utf8'})
}
}
return ''
}

function wrapToToggleBlock(header: string, body: string): string {
return `<details>
<summary>${header}</summary>
${body}
</details>`
}

function getViewReportText(reportUrl: string): string {
if (reportUrl !== '') {
return `☁️ [View the detailed Qodana report](${reportUrl})`
}
return wrapToToggleBlock(
'View the detailed Qodana report',
VIEW_REPORT_OPTIONS
)
}

/**
* Generates a table row for a given level.
* @param annotations The annotations to generate the table row from.
* @param level The level to generate the table row for.
*/
function getRowsByLevel(annotations: Annotation[], level: string): string {
const problems = annotations.reduce(
(map: Map<string, number>, e) =>
map.set(
e.title ?? UNKNOWN_RULE_ID,
map.get(e.title ?? UNKNOWN_RULE_ID) !== undefined
? map.get(e.title ?? UNKNOWN_RULE_ID)! + 1
: 1
),
new Map()
)
return Array.from(problems.entries())
.sort((a, b) => b[1] - a[1])
.map(([title, count]) => `| \`${title}\` | ${level} | ${count} |`)
.join('\n')
}

/**
* Generates action summary string of annotations.
* @param toolName The name of the tool to generate the summary from.
* @param projectDir The path to the project.
* @param annotations The annotations to generate the summary from.
* @param coverageInfo The coverage is a Markdown text to generate the summary from.
* @param packages The number of dependencies in the analyzed project.
* @param licensesInfo The licenses a Markdown text to generate the summary from.
* @param reportUrl The URL to the Qodana report.
* @param prMode Whether the analysis was run in the pull request mode.
* @param dependencyCharsLimit Limit on how many characters can be included in comment
*/
export function getSummary(
toolName: string,
projectDir: string,
annotations: Annotation[],
coverageInfo: string,
packages: number,
licensesInfo: string,
reportUrl: string,
prMode: boolean,
dependencyCharsLimit: number
): string {
const contactBlock = wrapToToggleBlock('Contact Qodana team', SUMMARY_MISC)
let licensesBlock = ''
if (licensesInfo !== '' && licensesInfo.length < dependencyCharsLimit) {
licensesBlock = wrapToToggleBlock(
`Detected ${packages} ${getDepencencyPlural(packages)}`,
licensesInfo
)
}
let prModeBlock = ''
if (prMode) {
prModeBlock = SUMMARY_PR_MODE
}
if (reportUrl !== '') {
const firstToolName = toolName.split(' ')[0]
toolName = toolName.replace(
firstToolName,
`[${firstToolName}](${reportUrl})`
)
}
if (annotations.length === 0) {
return [
`# ${toolName}`,
projectDir === '' ? '' : ['`', projectDir, '/`\n'].join(''),
'**It seems all right 👌**',
'',
'No new problems were found according to the checks applied',
coverageInfo,
prModeBlock,
getViewReportText(reportUrl),
licensesBlock,
contactBlock
].join('\n')
}

return [
`# ${toolName}`,
projectDir === '' ? '' : ['`', projectDir, '/`\n'].join(''),
`**${annotations.length} ${getProblemPlural(
annotations.length
)}** were found`,
'',
SUMMARY_TABLE_HEADER,
SUMMARY_TABLE_SEP,
[
getRowsByLevel(
annotations.filter(a => a.level === FAILURE_LEVEL),
'🔴 Failure'
),
getRowsByLevel(
annotations.filter(a => a.level === WARNING_LEVEL),
'🔶 Warning'
),
getRowsByLevel(
annotations.filter(a => a.level === NOTICE_LEVEL),
'◽️ Notice'
)
]
.filter(e => e !== '')
.join('\n'),
'',
coverageInfo,
prModeBlock,
getViewReportText(reportUrl),
licensesBlock,
contactBlock
].join('\n')
}

/**
* Generates a plural form of the word "problem" depending on the given count.
* @param count A number representing the count of problems
* @returns A formatted string with the correct plural form of "problem"
*/
export function getProblemPlural(count: number): string {
return `new problem${count !== 1 ? 's' : ''}`
}

/**
* Generates a plural form of the word "dependency" depending on the given count.
* @param count A number representing the count of dependencies
* @returns A formatted string with the correct plural form of "dependency"
*/
export function getDepencencyPlural(count: number): string {
return `dependenc${count !== 1 ? 'ies' : 'y'}`
}
2 changes: 1 addition & 1 deletion common/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
},
"types": ".qodana.d.ts",
"files": ["cli.json", "qodana.ts"]
"files": ["cli.json", "qodana.ts", "output.ts", "annotations.ts", "utils.ts"]
}
58 changes: 58 additions & 0 deletions common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type {Tool} from 'sarif'

Check failure on line 17 in common/utils.ts

View workflow job for this annotation

GitHub Actions / Qodana for JVM

ESLint

ESLint: Parsing error: The keyword 'import' is reserved

export interface Rule {
shortDescription: string
fullDescription: string
}

/**
* Extracts the rules descriptions from SARIF tool field.
* @param tool the SARIF tool field.
* @returns The map of SARIF rule IDs to their descriptions.
*/
export function parseRules(tool: Tool): Map<string, Rule> {
const rules = new Map<string, Rule>()
tool.driver.rules?.forEach(rule => {
rules.set(rule.id, {
shortDescription: rule.shortDescription!.text,
fullDescription:
rule.fullDescription!.markdown || rule.fullDescription!.text
})
})

tool?.extensions?.forEach(ext => {
ext?.rules?.forEach(rule => {
rules.set(rule.id, {
shortDescription: rule.shortDescription!.text,
fullDescription:
rule.fullDescription!.markdown || rule.fullDescription!.text
})
})
})
return rules
}

export function hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return hash;
}
Loading

0 comments on commit 1a63fb8

Please sign in to comment.