Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure integration improvements #438

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
280 changes: 280 additions & 0 deletions common/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/*
* 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 COMMIT_USER = 'qodana-bot'
export const COMMIT_EMAIL = 'qodana-support@jetbrains.com'
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 [qodana-support@jetbrains.com](mailto:qodana-support@jetbrains.com)
- 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'}`
}

/*
* The pull request with quick-fixes body template.
*/
export function prFixesBody(jobUrl: string): string {
return ` 🖐 Hey there!

This pull request has been auto-generated by the [Qodana Scan workflow](${jobUrl}) configured in your repository.
It has performed code analysis and applied some suggested fixes to improve your code quality 🧹✨

> **Warning**
> It's crucial to review these changes to ensure everything shipshape manually. Please take a moment to examine the changes here. Remember to run your integration tests against this PR to validate the fixes and ensure everything's functioning as expected.

_💻🔍 Happy reviewing and testing!
Best,
[Qodana Scan 🤖](https://github.com/marketplace/actions/qodana-scan)_`
}
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"]
}
49 changes: 49 additions & 0 deletions common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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
}
Loading
Loading