mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +00:00
e0cec3dc95
feat(lint): add `types-location` rule and enable it for `lib/modules`
Reviewers consistently ask for exported types to be moved into a
colocated `types.ts`. Add a `renovate/types-location` oxlint rule that
reports `export interface` / `export type` declarations outside such a
file, and turn it on across `lib/modules`.
Re-exports (`export type { X }`, `export type *`), file-local types, and
values (enums, classes, functions) are left alone. `schema.ts` files and
`schema/` directories are excluded via config, matching how
`zod-schema-location` is configured — inferred types belong next to the
schema they are derived from.
Relocating the 87 existing violations means:
- new `types.ts` files for the modules that had none
- `terragrunt/common.ts` folded into its `types.ts`, as it held nothing
else
- `RangeBound` in `versioning/gradle/compare.ts` un-exported, since only
a file-local interface referenced it
- `bazel-module/parser/fragments.ts` split from its inferred types, which
now live in `parser/types.ts` and infer through a namespace import so
each schema keeps its merged name
421 lines
11 KiB
TypeScript
421 lines
11 KiB
TypeScript
import { isNotNullOrUndefined } from '../../../util/array.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { parseVersion } from '../nuget/parser.ts';
|
|
import type { NugetVersion } from '../nuget/types.ts';
|
|
import { versionToString } from '../nuget/version.ts';
|
|
import type {
|
|
PaketConstraint,
|
|
PaketInterval,
|
|
PaketOperator,
|
|
PaketRange,
|
|
} from './types.ts';
|
|
import { compare, sameReleaseParts } from './version.ts';
|
|
|
|
const constraintOperators: readonly string[] = [
|
|
'=',
|
|
'==',
|
|
'>',
|
|
'>=',
|
|
'<',
|
|
'<=',
|
|
'~>',
|
|
];
|
|
|
|
const prereleaseTagRegex = regEx(/^[a-zA-Z][a-zA-Z0-9-]*$/);
|
|
|
|
/**
|
|
* Paket only accepts these operator combinations for two-part constraints.
|
|
*/
|
|
function isValidPair(first: PaketOperator, second: PaketOperator): boolean {
|
|
if (first === '~>') {
|
|
return (
|
|
second === '>' || second === '>=' || second === '<' || second === '<='
|
|
);
|
|
}
|
|
|
|
if (first === '>' || first === '>=') {
|
|
return second === '<' || second === '<=';
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function parseRange(input: string): PaketRange | null {
|
|
if (!input?.trim()) {
|
|
return null;
|
|
}
|
|
|
|
let text = input.trim();
|
|
let strategy: PaketRange['strategy'] = null;
|
|
if (text.startsWith('!') || text.startsWith('@')) {
|
|
strategy = text[0] as '!' | '@';
|
|
text = text.slice(1).trim();
|
|
}
|
|
|
|
const tokens = text.split(regEx(/\s+/));
|
|
const constraints: PaketConstraint[] = [];
|
|
let idx = 0;
|
|
|
|
while (
|
|
idx < tokens.length &&
|
|
constraints.length < 2 &&
|
|
constraintOperators.includes(tokens[idx])
|
|
) {
|
|
const operator = tokens[idx] as PaketOperator;
|
|
const versionToken = tokens[idx + 1];
|
|
const version = versionToken ? parseVersion(versionToken) : null;
|
|
if (!version) {
|
|
return null;
|
|
}
|
|
constraints.push({ operator, version });
|
|
idx += 2;
|
|
}
|
|
|
|
if (constraints.length === 0) {
|
|
const version = parseVersion(tokens[0]);
|
|
if (!version) {
|
|
return null;
|
|
}
|
|
constraints.push({ operator: '', version });
|
|
idx = 1;
|
|
}
|
|
|
|
if (
|
|
constraints.length === 2 &&
|
|
!isValidPair(constraints[0].operator, constraints[1].operator)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const prereleaseTags: string[] = [];
|
|
for (const token of tokens.slice(idx)) {
|
|
if (!prereleaseTagRegex.test(token)) {
|
|
return null;
|
|
}
|
|
if (!prereleaseTags.includes(token)) {
|
|
prereleaseTags.push(token);
|
|
}
|
|
}
|
|
|
|
return { strategy, constraints, prereleaseTags };
|
|
}
|
|
|
|
export function rangeToString(range: PaketRange): string {
|
|
const parts: string[] = [];
|
|
for (const { operator, version } of range.constraints) {
|
|
const versionStr = versionToString(version);
|
|
parts.push(operator === '' ? versionStr : `${operator} ${versionStr}`);
|
|
}
|
|
parts.push(...range.prereleaseTags);
|
|
return `${range.strategy ?? ''}${parts.join(' ')}`;
|
|
}
|
|
|
|
export function releaseParts(version: NugetVersion): number[] {
|
|
const { major, minor, patch, revision } = version;
|
|
return [major, minor, patch, revision].filter(isNotNullOrUndefined);
|
|
}
|
|
|
|
function versionFromParts(parts: number[]): NugetVersion {
|
|
const [major, minor, patch, revision] = parts;
|
|
const result: NugetVersion = { type: 'nuget-version', major };
|
|
if (minor !== undefined) {
|
|
result.minor = minor;
|
|
}
|
|
if (patch !== undefined) {
|
|
result.patch = patch;
|
|
}
|
|
if (revision !== undefined) {
|
|
result.revision = revision;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function adaptToPrecision(
|
|
version: NugetVersion,
|
|
precision: number,
|
|
): NugetVersion {
|
|
const parts = releaseParts(version).slice(0, precision);
|
|
while (parts.length < precision) {
|
|
parts.push(0);
|
|
}
|
|
return versionFromParts(parts);
|
|
}
|
|
|
|
export function bumpAtPrecision(
|
|
version: NugetVersion,
|
|
precision: number,
|
|
): NugetVersion {
|
|
const parts = releaseParts(adaptToPrecision(version, precision));
|
|
parts[precision - 1] += 1;
|
|
return versionFromParts(parts);
|
|
}
|
|
|
|
/**
|
|
* Upper bound of the pessimistic operator: chop off the last release part and
|
|
* increment the remaining last number, so `~> 1.2.3` allows `< 1.3` and
|
|
* `~> 1.2` allows `< 2`.
|
|
*/
|
|
export function twiddle(version: NugetVersion): NugetVersion {
|
|
const precision = Math.max(releaseParts(version).length - 1, 1);
|
|
return bumpAtPrecision(version, precision);
|
|
}
|
|
|
|
function intervalOfPair(
|
|
first: PaketConstraint,
|
|
second: PaketConstraint,
|
|
): PaketInterval {
|
|
if (first.operator === '~>') {
|
|
const cap = twiddle(first.version);
|
|
|
|
if (second.operator === '>' || second.operator === '>=') {
|
|
return {
|
|
kind: 'range',
|
|
from: second.version,
|
|
fromInclusive: second.operator === '>=',
|
|
to: cap,
|
|
toInclusive: false,
|
|
};
|
|
}
|
|
|
|
const to = compare(second.version, cap) < 0 ? second.version : cap;
|
|
return {
|
|
kind: 'range',
|
|
from: first.version,
|
|
fromInclusive: true,
|
|
to,
|
|
toInclusive: second.operator === '<=',
|
|
};
|
|
}
|
|
|
|
return {
|
|
kind: 'range',
|
|
from: first.version,
|
|
fromInclusive: first.operator === '>=',
|
|
to: second.version,
|
|
toInclusive: second.operator === '<=',
|
|
};
|
|
}
|
|
|
|
const singleOperatorKind = {
|
|
'': 'specific',
|
|
'=': 'specific',
|
|
'==': 'override',
|
|
'>=': 'minimum',
|
|
'>': 'greater-than',
|
|
'<=': 'maximum',
|
|
'<': 'less-than',
|
|
} as const;
|
|
|
|
export function intervalOf(constraints: PaketConstraint[]): PaketInterval {
|
|
if (constraints.length === 2) {
|
|
return intervalOfPair(constraints[0], constraints[1]);
|
|
}
|
|
|
|
const [{ operator, version }] = constraints;
|
|
if (operator === '~>') {
|
|
return {
|
|
kind: 'range',
|
|
from: version,
|
|
fromInclusive: true,
|
|
to: twiddle(version),
|
|
toInclusive: false,
|
|
};
|
|
}
|
|
return { kind: singleOperatorKind[operator], version };
|
|
}
|
|
|
|
const numericSegmentRegex = regEx(/^\d+$/);
|
|
const channelPrefixRegex = regEx(/^[a-zA-Z]+(?:-[a-zA-Z]+)*/);
|
|
|
|
function channelFromSegments(segments: string[]): string {
|
|
const [first, second] = segments;
|
|
if (first && !numericSegmentRegex.test(first)) {
|
|
return first;
|
|
}
|
|
if (second && !numericSegmentRegex.test(second)) {
|
|
return second;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* The channel name of a prerelease suffix, e.g. `alpha` for `alpha001` or
|
|
* `beta` for `beta.2`.
|
|
*/
|
|
export function prereleaseChannel(prerelease: string): string {
|
|
const segments = prerelease.split('.');
|
|
if (segments.length > 1) {
|
|
return channelFromSegments(segments);
|
|
}
|
|
|
|
const prefix = channelPrefixRegex.exec(prerelease);
|
|
if (prefix) {
|
|
return prefix[0];
|
|
}
|
|
|
|
return channelFromSegments(
|
|
prerelease.split('-').filter((segment) => segment !== ''),
|
|
);
|
|
}
|
|
|
|
type PrereleaseStatus = 'none' | 'all' | string[];
|
|
|
|
function prereleaseStatusOf(
|
|
range: PaketRange,
|
|
interval: PaketInterval,
|
|
): PrereleaseStatus {
|
|
const { prereleaseTags } = range;
|
|
if (
|
|
prereleaseTags.length === 1 &&
|
|
prereleaseTags[0].toLowerCase() === 'prerelease'
|
|
) {
|
|
return 'all';
|
|
}
|
|
if (prereleaseTags.length > 0) {
|
|
return prereleaseTags;
|
|
}
|
|
|
|
// Without explicit tags, prerelease versions used in the constraint itself
|
|
// allow their own channels, e.g. `~> 1.2.3-alpha001` allows `alpha` versions.
|
|
const bounds =
|
|
interval.kind === 'range'
|
|
? [interval.from, interval.to]
|
|
: [interval.version];
|
|
const channels: string[] = [];
|
|
for (const bound of bounds) {
|
|
if (!bound.prerelease) {
|
|
continue;
|
|
}
|
|
const channel = prereleaseChannel(bound.prerelease);
|
|
if (!channels.includes(channel)) {
|
|
channels.push(channel);
|
|
}
|
|
}
|
|
return channels.length > 0 ? channels : 'none';
|
|
}
|
|
|
|
function isPrereleaseAllowed(
|
|
version: NugetVersion,
|
|
status: PrereleaseStatus,
|
|
): boolean {
|
|
if (status === 'all') {
|
|
return true;
|
|
}
|
|
if (status === 'none') {
|
|
return !version.prerelease;
|
|
}
|
|
if (!version.prerelease) {
|
|
return true;
|
|
}
|
|
return status.includes(prereleaseChannel(version.prerelease));
|
|
}
|
|
|
|
/**
|
|
* Paket rule: a prerelease of a stable bound version counts as being in range
|
|
* when its channel is allowed, e.g. `1.2.3-alpha1` satisfies `= 1.2.3 alpha`.
|
|
*/
|
|
function isPrereleaseOfBound(
|
|
version: NugetVersion,
|
|
bound: NugetVersion,
|
|
status: PrereleaseStatus,
|
|
): boolean {
|
|
return (
|
|
status !== 'none' &&
|
|
!bound.prerelease &&
|
|
sameReleaseParts(version, bound) &&
|
|
isPrereleaseAllowed(version, status)
|
|
);
|
|
}
|
|
|
|
export function matches(version: NugetVersion, range: PaketRange): boolean {
|
|
const interval = intervalOf(range.constraints);
|
|
const status = prereleaseStatusOf(range, interval);
|
|
|
|
switch (interval.kind) {
|
|
case 'specific':
|
|
return (
|
|
compare(version, interval.version) === 0 ||
|
|
isPrereleaseOfBound(version, interval.version, status)
|
|
);
|
|
case 'override':
|
|
return compare(version, interval.version) === 0;
|
|
case 'minimum': {
|
|
const cmp = compare(version, interval.version);
|
|
return (
|
|
cmp === 0 ||
|
|
(cmp > 0 && isPrereleaseAllowed(version, status)) ||
|
|
isPrereleaseOfBound(version, interval.version, status)
|
|
);
|
|
}
|
|
case 'greater-than':
|
|
return (
|
|
compare(version, interval.version) > 0 &&
|
|
isPrereleaseAllowed(version, status)
|
|
);
|
|
case 'maximum': {
|
|
const cmp = compare(version, interval.version);
|
|
return cmp === 0 || (cmp < 0 && isPrereleaseAllowed(version, status));
|
|
}
|
|
case 'less-than':
|
|
return (
|
|
compare(version, interval.version) < 0 &&
|
|
isPrereleaseAllowed(version, status) &&
|
|
!isPrereleaseOfBound(version, interval.version, status)
|
|
);
|
|
case 'range': {
|
|
const { from, fromInclusive, to, toInclusive } = interval;
|
|
const lowerCmp = compare(version, from);
|
|
const upperCmp = compare(version, to);
|
|
const inLower = fromInclusive ? lowerCmp >= 0 : lowerCmp > 0;
|
|
const inUpper = toInclusive
|
|
? upperCmp <= 0
|
|
: upperCmp < 0 && !isPrereleaseOfBound(version, to, status);
|
|
return (
|
|
(inLower && inUpper && isPrereleaseAllowed(version, status)) ||
|
|
isPrereleaseOfBound(version, from, status)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function isLessThanLowerBound(
|
|
version: NugetVersion,
|
|
range: PaketRange,
|
|
): boolean {
|
|
const interval = intervalOf(range.constraints);
|
|
switch (interval.kind) {
|
|
case 'specific':
|
|
case 'override':
|
|
case 'minimum':
|
|
return compare(version, interval.version) < 0;
|
|
case 'greater-than':
|
|
return compare(version, interval.version) <= 0;
|
|
case 'maximum':
|
|
case 'less-than':
|
|
return false;
|
|
case 'range':
|
|
return interval.fromInclusive
|
|
? compare(version, interval.from) < 0
|
|
: compare(version, interval.from) <= 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A rewritten range admits the new version numerically, but its prerelease
|
|
* channel may still be blocked, so allow the channel with an explicit tag.
|
|
*/
|
|
export function ensurePrereleaseMatches(
|
|
range: PaketRange,
|
|
version: NugetVersion,
|
|
): PaketRange {
|
|
if (!version.prerelease || matches(version, range)) {
|
|
return range;
|
|
}
|
|
|
|
const channel = prereleaseChannel(version.prerelease);
|
|
if (prereleaseTagRegex.test(channel)) {
|
|
return { ...range, prereleaseTags: [...range.prereleaseTags, channel] };
|
|
}
|
|
return { ...range, prereleaseTags: ['prerelease'] };
|
|
}
|