Implement support for language aliasing

This commit is contained in:
Henry Mercer 2023-09-14 15:26:38 +01:00
parent e982de4fb4
commit 74714a34ca
15 changed files with 121 additions and 61 deletions

View file

@ -212,6 +212,9 @@ export interface ResolveLanguagesOutput {
}
export interface BetterResolveLanguagesOutput {
aliases?: {
[alias: string]: string;
};
extractors: {
[language: string]: [
{
@ -330,6 +333,11 @@ export const CODEQL_VERSION_RESOLVE_ENVIRONMENT = "2.13.4";
*/
export const CODEQL_VERSION_LANGUAGE_BASELINE_CONFIG = "2.14.2";
/**
* Versions 2.14.4+ of the CodeQL CLI support language aliasing.
*/
export const CODEQL_VERSION_LANGUAGE_ALIASING = "2.14.4";
/**
* Set up CodeQL CLI access.
*
@ -710,11 +718,20 @@ export async function getCodeQLForCmd(
}
},
async betterResolveLanguages() {
const extraArgs: string[] = [];
if (
await util.codeQlVersionAbove(this, CODEQL_VERSION_LANGUAGE_ALIASING)
) {
extraArgs.push("--extractor-include-aliases");
}
const codeqlArgs = [
"resolve",
"languages",
"--format=betterjson",
"--extractor-options-verbosity=4",
...extraArgs,
...getExtraOptionsFromEnv(["resolve", "languages"]),
];
const output = await runTool(cmd, codeqlArgs);

View file

@ -9,6 +9,7 @@ import * as api from "./api-client";
import {
CodeQL,
CODEQL_VERSION_GHES_PACK_DOWNLOAD,
CODEQL_VERSION_LANGUAGE_ALIASING,
CODEQL_VERSION_SECURITY_EXPERIMENTAL_SUITE,
ResolveQueriesOutput,
} from "./codeql";
@ -19,12 +20,7 @@ import {
logCodeScanningConfigInCli,
useCodeScanningConfigInCli,
} from "./feature-flags";
import {
Language,
LanguageOrAlias,
parseLanguage,
resolveAlias,
} from "./languages";
import { Language, parseLanguage } from "./languages";
import { Logger } from "./logging";
import { RepositoryNwo } from "./repository";
import { downloadTrapCaches } from "./trap-caching";
@ -874,7 +870,7 @@ export function getNoLanguagesError(): string {
}
export function getUnknownLanguagesError(languages: string[]): string {
return `Did not recognise the following languages: ${languages.join(", ")}`;
return `Did not recognize the following languages: ${languages.join(", ")}`;
}
/**
@ -884,7 +880,7 @@ export function getUnknownLanguagesError(languages: string[]): string {
export async function getLanguagesInRepo(
repository: RepositoryNwo,
logger: Logger,
): Promise<LanguageOrAlias[]> {
): Promise<Language[]> {
logger.debug(`GitHub repo ${repository.owner} ${repository.repo}`);
const response = await api.getApiClient().rest.repos.listLanguages({
owner: repository.owner,
@ -897,7 +893,7 @@ export async function getLanguagesInRepo(
// When we pick a language to autobuild we want to pick the most popular traced language
// Since sets in javascript maintain insertion order, using a set here and then splatting it
// into an array gives us an array of languages ordered by popularity
const languages: Set<LanguageOrAlias> = new Set();
const languages: Set<Language> = new Set();
for (const lang of Object.keys(response.data as Record<string, number>)) {
const parsedLang = parseLanguage(lang);
if (parsedLang !== undefined) {
@ -930,13 +926,22 @@ export async function getLanguages(
logger,
);
let languages = rawLanguages.map(resolveAlias);
let languages = rawLanguages;
if (autodetected) {
const availableLanguages = await codeQL.resolveLanguages();
languages = languages.filter((value) => value in availableLanguages);
const supportedLanguages = Object.keys(await codeQL.resolveLanguages());
languages = languages
.map(parseLanguage)
.filter((value) => value && supportedLanguages.includes(value))
.map((value) => value as Language);
logger.info(`Automatically detected languages: ${languages.join(", ")}`);
} else {
const aliases = await getLanguageAliases(codeQL);
if (aliases) {
languages = languages.map((lang) => aliases[lang] || lang);
}
logger.info(`Languages from configuration: ${languages.join(", ")}`);
}
@ -950,7 +955,6 @@ export async function getLanguages(
const parsedLanguages: Language[] = [];
const unknownLanguages: string[] = [];
for (const language of languages) {
// We know this is not an alias since we resolved it above.
const parsedLanguage = parseLanguage(language) as Language;
if (parsedLanguage === undefined) {
unknownLanguages.push(language);
@ -968,6 +972,19 @@ export async function getLanguages(
return parsedLanguages;
}
/**
* Gets the set of languages supported by CodeQL, along with their aliases if supported by the
* version of the CLI.
*/
export async function getLanguageAliases(
codeql: CodeQL,
): Promise<{ [alias: string]: string } | undefined> {
if (await codeQlVersionAbove(codeql, CODEQL_VERSION_LANGUAGE_ALIASING)) {
return (await codeql.betterResolveLanguages()).aliases;
}
return undefined;
}
/**
* Gets the set of languages in the current repository without checking to
* see if these languages are actually supported by CodeQL.

View file

@ -20,15 +20,15 @@ test("parseLanguage", async (t) => {
t.deepEqual(parseLanguage("python"), Language.python);
// Aliases
t.deepEqual(parseLanguage("c"), "c");
t.deepEqual(parseLanguage("c++"), "c++");
t.deepEqual(parseLanguage("c#"), "c#");
t.deepEqual(parseLanguage("kotlin"), "kotlin");
t.deepEqual(parseLanguage("typescript"), "typescript");
t.deepEqual(parseLanguage("c"), Language.cpp);
t.deepEqual(parseLanguage("c++"), Language.cpp);
t.deepEqual(parseLanguage("c#"), Language.csharp);
t.deepEqual(parseLanguage("kotlin"), Language.java);
t.deepEqual(parseLanguage("typescript"), Language.javascript);
// spaces and case-insensitivity
t.deepEqual(parseLanguage(" \t\nCsHaRp\t\t"), Language.csharp);
t.deepEqual(parseLanguage(" \t\nkOtLin\t\t"), "kotlin");
t.deepEqual(parseLanguage(" \t\nkOtLin\t\t"), Language.java);
// Not matches
t.deepEqual(parseLanguage("foo"), undefined);

View file

@ -19,23 +19,16 @@ export const LANGUAGE_ALIASES: { [lang: string]: Language } = {
typescript: Language.javascript,
};
export type LanguageOrAlias = Language | keyof typeof LANGUAGE_ALIASES;
export function resolveAlias(lang: LanguageOrAlias): Language {
return LANGUAGE_ALIASES[lang] || lang;
}
/**
* Translate from user input or GitHub's API names for languages to CodeQL's
* names for languages. This does not translate a language alias to the actual
* language used by CodeQL.
* names for languages.
*
* @param language The language to translate.
* @returns A language supported by CodeQL, an alias for a language, or
* `undefined` if the input language cannot be parsed into a langauge supported
* `undefined` if the input language cannot be parsed into a language supported
* by CodeQL.
*/
export function parseLanguage(language: string): LanguageOrAlias | undefined {
export function parseLanguage(language: string): Language | undefined {
// Normalise to lower case
language = language.trim().toLowerCase();
@ -47,7 +40,7 @@ export function parseLanguage(language: string): LanguageOrAlias | undefined {
// Check language aliases, but return the original language name,
// the alias will be resolved later.
if (language in LANGUAGE_ALIASES) {
return language;
return LANGUAGE_ALIASES[language];
}
return undefined;

View file

@ -8,7 +8,7 @@ import {
import { getGitHubVersion } from "./api-client";
import { CommandInvocationError } from "./codeql";
import * as configUtils from "./config-utils";
import { Language, resolveAlias } from "./languages";
import { Language, parseLanguage } from "./languages";
import { getActionsLogger } from "./logging";
import { runResolveBuildEnvironment } from "./resolve-environment";
import {
@ -29,7 +29,6 @@ const ENVIRONMENT_OUTPUT_NAME = "environment";
async function run() {
const startedAt = new Date();
const logger = getActionsLogger();
const language: Language = resolveAlias(getRequiredInput("language"));
try {
if (
@ -45,6 +44,16 @@ async function run() {
return;
}
const language: Language | undefined = parseLanguage(
getRequiredInput("language"),
);
if (language === undefined) {
throw new Error(
`Did not recognize the language "${getRequiredInput("language")}".`,
);
}
const gitHubVersion = await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger);