Files
Sebastian Poxhofer 97327c3d28 refactor(linters): add prefer-coerce-array rule (#45492)
* refactor(linters): add prefer-coerce-array rule

Refs #45217

`x ?? []` and `coerceArray(x)` say the same thing, but only the second
one is searchable and consistent. The rule flags a `??` whose default is
a bare `[]` literal; a non-empty default or an asserted `([] as Foo[])`
still passes.

Applying it across `lib/` surfaced three spots the helper cannot cover,
which keep `?? []` with a disable comment or a narrower fix:

- `package-files.ts` destructures a tuple, which `coerceArray()` widens
- `generate.spec.ts` defaults `matchAll()`, an iterator rather than an array
- `mise/upgradeable-tooling.ts` declares a `Record` return, so its
  `?? []` fallback was wrong to begin with and becomes `coerceObject()`

* refactor(platform/github): hoist the GHE version header lookup

`Object.entries(...).find()` returns a tuple, and defaulting it with
`?? []` only to destructure the second element reads as an array
default when it is not one. Name the found entry instead, which drops
the `?? []` rather than routing a tuple through `coerceArray()`.
2026-08-28 09:27:58 +00:00

182 lines
5.4 KiB
TypeScript

// SEE for the reference https://github.com/renovatebot/renovate/blob/c3e9e572b225085448d94aa121c7ec81c14d3955/lib/platform/bitbucket/utils.js
import { isNonEmptyString } from '@sindresorhus/is';
import { CONFIG_GIT_URL_UNAVAILABLE } from '../../../constants/error-messages.ts';
import { logger } from '../../../logger/index.ts';
import type { GitOptions, GitProtocol } from '../../../types/git.ts';
import type { HostRule } from '../../../types/index.ts';
import { coerceArray } from '../../../util/array.ts';
import * as git from '../../../util/git/index.ts';
import { regEx } from '../../../util/regex.ts';
import { ensureTrailingSlash, parseUrl } from '../../../util/url.ts';
import { getPrBodyStruct } from '../pr-body.ts';
import type { GitUrlOption } from '../types.ts';
import type { BbsPr, BbsRestPr, BbsRestRepo, BitbucketError } from './types.ts';
export const BITBUCKET_INVALID_REVIEWERS_EXCEPTION =
'com.atlassian.bitbucket.pull.InvalidPullRequestReviewersException';
// https://docs.atlassian.com/bitbucket-server/rest/6.0.0/bitbucket-rest.html#idp250
const prStateMapping: any = {
MERGED: 'merged',
DECLINED: 'closed',
OPEN: 'open',
};
export function prInfo(pr: BbsRestPr): BbsPr {
return {
version: pr.version,
number: pr.id,
bodyStruct: getPrBodyStruct(pr.description),
sourceBranch: pr.fromRef.displayId,
targetBranch: pr.toRef.displayId,
title: pr.title,
state: prStateMapping[pr.state],
createdAt: pr.createdDate,
};
}
export function isInvalidReviewersResponse(err: BitbucketError): boolean {
const errors = coerceArray(err?.response?.body?.errors);
return (
errors.length > 0 &&
errors.every(
(error) => error.exceptionName === BITBUCKET_INVALID_REVIEWERS_EXCEPTION,
)
);
}
export function getInvalidReviewers(err: BitbucketError): string[] {
const errors = coerceArray(err?.response?.body?.errors);
let invalidReviewers: string[] = [];
for (const error of errors) {
// v8 ignore else -- TODO: add test #40625
if (error.exceptionName === BITBUCKET_INVALID_REVIEWERS_EXCEPTION) {
invalidReviewers = invalidReviewers.concat(
coerceArray(
error.reviewerErrors
?.map(({ context }) => context)
.filter(isNonEmptyString),
),
);
}
}
return invalidReviewers;
}
function generateUrlFromEndpoint(
defaultEndpoint: string,
opts: HostRule,
repository: string,
): string {
const url = parseUrl(defaultEndpoint);
if (!url) {
throw new Error(`Invalid Bitbucket Server endpoint: ${defaultEndpoint}`);
}
const authString =
opts.username && opts.password
? `${opts.username}:${opts.password}`
: (opts.username ?? '');
const generatedUrl = git.getUrl({
protocol: url.protocol as GitProtocol,
// TODO: types (#22198)
auth: authString,
host: `${url.host}${ensureTrailingSlash(url.pathname)}scm`,
repository,
});
logger.debug(`Using generated endpoint URL: ${generatedUrl}`);
return generatedUrl;
}
function injectAuth(url: string, opts: HostRule): string {
const repoUrl = parseUrl(url);
if (!repoUrl) {
logger.debug(`Invalid url: ${url}`);
throw new Error(CONFIG_GIT_URL_UNAVAILABLE);
}
// v8 ignore else -- TODO: add test #40625
if (!opts.token && opts.username && opts.password) {
repoUrl.username = opts.username;
repoUrl.password = opts.password;
}
return repoUrl.toString();
}
export function getRepoGitUrl(
repository: string,
defaultEndpoint: string,
gitUrl: GitUrlOption | undefined,
info: BbsRestRepo,
opts: HostRule,
): string {
switch (gitUrl) {
case 'endpoint': {
const generatedUrl = generateUrlFromEndpoint(
defaultEndpoint,
opts,
repository,
);
logger.debug(`Using endpoint URL: ${generatedUrl}`);
return generatedUrl;
}
case 'ssh': {
const sshUrl = info.links.clone?.find(({ name }) => name === 'ssh');
if (sshUrl === undefined) {
throw new Error(CONFIG_GIT_URL_UNAVAILABLE);
}
logger.debug(`Using ssh URL: ${sshUrl.href}`);
return sshUrl.href;
}
case undefined:
case 'default': {
let cloneUrl = info.links.clone?.find(({ name }) => name === 'http');
if (cloneUrl) {
// Inject auth into the API provided URL
return injectAuth(cloneUrl.href, opts);
}
// Http access might be disabled, try to find ssh url in this case
cloneUrl = info.links.clone?.find(({ name }) => name === 'ssh');
if (cloneUrl) {
return cloneUrl.href;
}
// SSH urls can be used directly
return generateUrlFromEndpoint(defaultEndpoint, opts, repository);
}
}
}
export function getExtraCloneOpts(opts: HostRule): GitOptions {
if (opts.token) {
return {
'-c': `http.extraHeader=Authorization: Bearer ${opts.token}`,
};
}
return {};
}
export function splitEscapedSpaces(str: string): string[] {
const parts = str.split(' ');
const result: string[] = [];
let last: string | undefined;
for (const part of parts) {
if (last?.endsWith('\\\\')) {
result[result.length - 1] = `${last.slice(0, -2)} ${part}`;
} else {
result.push(part);
}
last = result.at(-1);
}
return result;
}
export function parseModifier(value: string): number | null {
const match = regEx('^random(?:\\((\\d+)\\))?$').exec(value);
if (!match) {
return null;
}
return parseInt(match[1] ?? '1', 10);
}