import { fixCodeQualityCategory, getOptionalInput, getRequiredInput, } from "./actions-util"; import { Logger } from "./logging"; import { ConfigurationError } from "./util"; export enum AnalysisKind { CodeScanning = "code-scanning", CodeQuality = "code-quality", } // Exported for testing. A set of all known analysis kinds. export const supportedAnalysisKinds = new Set(Object.values(AnalysisKind)); /** * Parses a comma-separated string into a list of unique analysis kinds. * Throws a configuration error if the input contains unknown analysis kinds * or doesn't contain at least one element. * * @param input The comma-separated string to parse. * @returns The array of unique analysis kinds that were parsed from the input string. */ export async function parseAnalysisKinds( input: string, ): Promise { const components = input.split(","); if (components.length < 1) { throw new ConfigurationError( "At least one analysis kind must be configured.", ); } for (const component of components) { if (!supportedAnalysisKinds.has(component as AnalysisKind)) { throw new ConfigurationError(`Unknown analysis kind: ${component}`); } } // Return all unique elements. return Array.from( new Set(components.map((component) => component as AnalysisKind)), ); } // Used to avoid re-parsing the input after we have done it once. let cachedAnalysisKinds: AnalysisKind[] | undefined; /** * Initialises the analysis kinds for the analysis based on the `analysis-kinds` input. * This function will also use the deprecated `quality-queries` input as an indicator to enable `code-quality`. * If the `analysis-kinds` input cannot be parsed, a `ConfigurationError` is thrown. * * @param logger The logger to use. * @param skipCache For testing, whether to ignore the cached values (default: false). * * @returns The array of enabled analysis kinds. * @throws A `ConfigurationError` if the `analysis-kinds` input cannot be parsed. */ export async function getAnalysisKinds( logger: Logger, skipCache: boolean = false, ): Promise { if (!skipCache && cachedAnalysisKinds !== undefined) { return cachedAnalysisKinds; } cachedAnalysisKinds = await parseAnalysisKinds( getRequiredInput("analysis-kinds"), ); // Warn that `quality-queries` is deprecated if there is an argument for it. const qualityQueriesInput = getOptionalInput("quality-queries"); if (qualityQueriesInput !== undefined) { logger.warning( "The `quality-queries` input is deprecated and will be removed in a future version of the CodeQL Action. " + "Use the `analysis-kinds` input to configure different analysis kinds instead.", ); } // For backwards compatibility, add Code Quality to the enabled analysis kinds // if an input to `quality-queries` was specified. We should remove this once // `quality-queries` is no longer used. if ( !cachedAnalysisKinds.includes(AnalysisKind.CodeQuality) && qualityQueriesInput !== undefined ) { cachedAnalysisKinds.push(AnalysisKind.CodeQuality); } return cachedAnalysisKinds; } /** The queries to use for Code Quality analyses. */ export const codeQualityQueries: string[] = ["code-quality"]; // Enumerates API endpoints that accept SARIF files. enum SARIF_UPLOAD_ENDPOINT { CODE_SCANNING = "PUT /repos/:owner/:repo/code-scanning/analysis", CODE_QUALITY = "PUT /repos/:owner/:repo/code-quality/analysis", } // Represents configurations for different analysis kinds. export interface AnalysisConfig { /** The analysis kind the configuration is for. */ kind: AnalysisKind; /** A display friendly name for logs. */ name: string; /** The API endpoint to upload SARIF files to. */ target: SARIF_UPLOAD_ENDPOINT; /** The file extension for SARIF files generated by this kind of analysis. */ sarifExtension: string; /** A predicate on filenames to decide whether a SARIF file * belongs to this kind of analysis. */ sarifPredicate: (name: string) => boolean; /** Analysis-specific adjustment of the category. */ fixCategory: (logger: Logger, category?: string) => string | undefined; /** A prefix for environment variables used to track the uniqueness of SARIF uploads. */ sentinelPrefix: string; } // Represents the Code Scanning analysis configuration. export const CodeScanning: AnalysisConfig = { kind: AnalysisKind.CodeScanning, name: "code scanning", target: SARIF_UPLOAD_ENDPOINT.CODE_SCANNING, sarifExtension: ".sarif", sarifPredicate: (name) => name.endsWith(CodeScanning.sarifExtension) && !CodeQuality.sarifPredicate(name), fixCategory: (_, category) => category, sentinelPrefix: "CODEQL_UPLOAD_SARIF_", }; // Represents the Code Quality analysis configuration. export const CodeQuality: AnalysisConfig = { kind: AnalysisKind.CodeQuality, name: "code quality", target: SARIF_UPLOAD_ENDPOINT.CODE_QUALITY, sarifExtension: ".quality.sarif", sarifPredicate: (name) => name.endsWith(CodeQuality.sarifExtension), fixCategory: fixCodeQualityCategory, sentinelPrefix: "CODEQL_UPLOAD_QUALITY_SARIF_", }; /** * Gets the `AnalysisConfig` corresponding to `kind`. * @param kind The analysis kind to get the `AnalysisConfig` for. * @returns The `AnalysisConfig` corresponding to `kind`. */ export function getAnalysisConfig(kind: AnalysisKind): AnalysisConfig { // Using a switch statement here accomplishes two things: // 1. The type checker believes us that we have a case for every `AnalysisKind`. // 2. If we ever add another member to `AnalysisKind`, the type checker will alert us that we have to add a case. switch (kind) { case AnalysisKind.CodeScanning: return CodeScanning; case AnalysisKind.CodeQuality: return CodeQuality; } } // Since we have overlapping extensions (i.e. ".sarif" includes ".quality.sarif"), // we want to scan a folder containing SARIF files in an order that finds the more // specific extensions first. This constant defines an array in the order of analyis // configurations with more specific extensions to less specific extensions. export const SarifScanOrder = [CodeQuality, CodeScanning];