@snyk/ruby-semver
Version:
node-semver compatible API with RubyGems semantics
162 lines (143 loc) • 4.29 kB
text/typescript
import { GemVersion, MaybeGemVersion } from './ruby/gem-version';
import { GemRequirement } from './ruby/gem-requirement';
import { compare, rcompare } from './comparison';
export {
validRange,
satisfies,
maxSatisfying,
minSatisfying,
intersects,
gtr,
ltr,
outside,
};
const gtr = (): never => {
throw new Error('Not implemented');
};
const ltr = (): never => {
throw new Error('Not implemented');
};
const outside = (): never => {
throw new Error('Not implemented');
};
function _createRequirement(range: string): GemRequirement {
return GemRequirement.create(range.split(','));
}
function _expandTildes(gemRequirement: GemRequirement) {
const requirements: string[] = [];
gemRequirement.requirements.forEach((req) => {
const op = req[0];
const version = req[1];
if (op.indexOf('~') !== -1) {
requirements.push(`>= ${version}`);
requirements.push(`< ${version.bump()}`);
} else {
requirements.push(`${op} ${version}`);
}
});
return GemRequirement.create(requirements);
}
function validRange(range: string): string | null {
if (range === null || range === undefined) {
return null;
}
if (range === '') {
return GemRequirement.default().toString();
}
try {
let requirement = _createRequirement(range);
if (range.indexOf('~') !== -1) {
requirement = _expandTildes(requirement);
}
return requirement.toString();
} catch (err) {
return null;
}
}
function satisfies(version: MaybeGemVersion, range: string): boolean {
try {
return _createRequirement(range).satisfiedBy(GemVersion.create(version));
} catch (err) {
return false;
}
}
function _firstSatisfying(
versions: MaybeGemVersion[],
range: string,
compareFunction,
): string {
const requirement = _createRequirement(range);
const maxSatisfying = versions
.map((v) => GemVersion.create(v))
.sort(compareFunction)
.find((v) => requirement.satisfiedBy(v));
return maxSatisfying ? maxSatisfying.toString() : null;
}
function maxSatisfying(versions: MaybeGemVersion[], range: string): string {
return _firstSatisfying(versions, range, rcompare);
}
function minSatisfying(versions: MaybeGemVersion[], range: string): string {
return _firstSatisfying(versions, range, compare);
}
function intersects(r1: string, r2: string): boolean {
try {
const range1 = _expandTildes(_createRequirement(r1));
const range2 = _expandTildes(_createRequirement(r2));
return (
intersectLeftRight(range1, range2) || intersectLeftRight(range2, range1)
);
} catch (e) {
return false;
}
}
function intersectLeftRight(leftRange, rightRange): boolean {
for (const [op, version] of leftRange.requirements) {
if (op === '!=') {
continue;
}
if (rightRange.satisfiedBy(version)) {
if (op === '' || op === '=' || op === '>=' || op === '<=') {
return true;
} else if (op === '<' || op === '>') {
const negOp = op === '<' ? '>=' : '<=';
if (
rightRange.requirements.every(
([op2, ver2]) =>
!(
(op2 === negOp || (op2 || '=') === '=') &&
ver2.compare(version) === 0
),
)
) {
return true;
}
}
} else {
// Handle cases where leftRange can intersect rightRange without any of
// leftRange's versions satisfying rightRange:
// 1. When both ranges have '>' or '<' parts that are identical.
if (op === '>' || op === '<') {
if (
rightRange.requirements.some(
([op2, ver2]) => (op2 || '=') === op && ver2.compare(version) === 0,
)
) {
return true;
}
}
// 2. When rightRange has a '!=' requirement that specifically excludes
// the edge, but otherwise it would have been included.
if (op === '>' || op === '>=' || op === '<' || op === '<=') {
if (rightRange.requirements.some(([op2]) => op2 === '!=')) {
const newRange = new GemRequirement(
rightRange.asList().filter((reqStr) => !reqStr.startsWith('!=')),
);
if (newRange.satisfiedBy(version)) {
return true;
}
}
}
}
}
return false;
}