Add baseline metrics for lines of code

This commit uses a third party library to estimate the lines of code in
a database that is to be analyzed by codeql.

The estimate uses the same includes and excludes globs for determining
which files should be counted.

The lines of code count is returned by language and injected into the
SARIF as `baseline` property in the `${language}/summary/lines-of-code`
metric.
This commit is contained in:
Andrew Eisenberg 2021-04-16 14:52:39 -07:00
parent 7c5b1287d5
commit 998f472183
11 changed files with 379 additions and 41 deletions

View file

@ -6,6 +6,7 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as analysisPaths from "./analysis-paths";
import { getCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { countLoc } from "./count-loc";
import { isScannedLanguage, Language } from "./languages";
import { Logger } from "./logging";
import * as sharedEnv from "./shared-environment";
@ -144,6 +145,18 @@ export async function runQueries(
): Promise<QueriesStatusReport> {
const statusReport: QueriesStatusReport = {};
// count the number of lines in the background
const locPromise = countLoc(
path.resolve(),
// config.paths specifies external directories. the current
// directory is included in the analysis by default. Replicate
// that here.
["**"].concat(config.paths || []),
config.pathsIgnore,
config.languages,
logger
);
for (const language of config.languages) {
logger.startGroup(`Analyzing ${language}`);
@ -157,13 +170,15 @@ export async function runQueries(
try {
if (queries["builtin"].length > 0) {
const startTimeBuliltIn = new Date().getTime();
await runQueryGroup(
const sarifFile = await runQueryGroup(
language,
"builtin",
queries["builtin"],
sarifFolder,
undefined
);
await injectLinesOfCode(sarifFile, language, locPromise);
statusReport[`analyze_builtin_queries_${language}_duration_ms`] =
new Date().getTime() - startTimeBuliltIn;
}
@ -172,23 +187,21 @@ export async function runQueries(
const temporarySarifFiles: string[] = [];
for (let i = 0; i < queries["custom"].length; ++i) {
if (queries["custom"][i].queries.length > 0) {
await runQueryGroup(
const sarifFile = await runQueryGroup(
language,
`custom-${i}`,
queries["custom"][i].queries,
temporarySarifDir,
queries["custom"][i].searchPath
);
temporarySarifFiles.push(
path.join(temporarySarifDir, `${language}-custom-${i}.sarif`)
);
temporarySarifFiles.push(sarifFile);
}
}
if (temporarySarifFiles.length > 0) {
fs.writeFileSync(
path.join(sarifFolder, `${language}-custom.sarif`),
combineSarifFiles(temporarySarifFiles)
);
const sarifFile = path.join(sarifFolder, `${language}-custom.sarif`);
fs.writeFileSync(sarifFile, combineSarifFiles(temporarySarifFiles));
await injectLinesOfCode(sarifFile, language, locPromise);
statusReport[`analyze_custom_queries_${language}_duration_ms`] =
new Date().getTime() - startTimeCustom;
}
@ -210,7 +223,7 @@ export async function runQueries(
queries: string[],
destinationFolder: string,
searchPath: string | undefined
): Promise<void> {
): Promise<string> {
const databasePath = util.getCodeQLDatabasePath(config.tempDir, language);
// Pass the queries to codeql using a file instead of using the command
// line to avoid command line length restrictions, particularly on windows.
@ -238,6 +251,8 @@ export async function runQueries(
`SARIF results for database ${language} created at "${sarifFile}"`
);
logger.endGroup();
return sarifFile;
}
}
@ -269,3 +284,30 @@ export async function runAnalyze(
return { ...queriesStats };
}
async function injectLinesOfCode(
sarifFile: string,
language: string,
locPromise: Promise<Record<string, number>>
) {
const lineCounts = await locPromise;
if (language in lineCounts) {
const sarif = JSON.parse(fs.readFileSync(sarifFile, "utf8"));
if (Array.isArray(sarif.runs)) {
for (const run of sarif.runs) {
const metricId = `${language}/summary/lines-of-code`;
run.properties = run.properties || {};
run.properties.metricResults = run.properties.metricResults || [];
const metric = run.properties.metricResults.find(
// the metric id can be in either of two places
(m) => m.metricId === metricId || m.metric?.id === metricId
);
// only add the baseline value if the metric already exists
if (metric) {
metric.baseline = lineCounts[language];
}
}
}
fs.writeFileSync(sarifFile, JSON.stringify(sarif));
}
}