// @flow import {findFlowSpecificVer} from './npm/npmProjectUtils'; import {ValidationError} from './ValidationError'; export type FlowSpecificVer = {| major: number, minor: number | 'x', patch: number | 'x', prerel: string | 'x' | null, |}; export type FlowVersion = | {|kind: 'all'|} | {|kind: 'specific', ver: FlowSpecificVer|} | {| kind: 'ranged', upper: FlowSpecificVer | null, lower: FlowSpecificVer | null, |}; function _parseVerNum( numStr: string, verName: string, context: string, ): number { const num = parseInt(numStr, 10); if (String(num) !== numStr) { throw new ValidationError( `'${context}': Invalid ${verName} number: '${numStr}'. Expected a number.`, ); } return num; } function _parseVerNumOrX( numStr: string | null | void, verName: string, context: string, ): number | 'x' { if (numStr == null) { return 'x'; } if (numStr === 'x') { return numStr; } return _parseVerNum(numStr, verName, context); } function _parseVersion( verStr: string, expectPossibleRangeUpper: boolean, ): [number, FlowSpecificVer] { if (verStr[0] !== 'v') { throw new ValidationError('Flow version ranges must start with a `v`!'); } const verParts = verStr .slice(1) .match(/^([0-9]+)\.([0-9]+|x)(\.([0-9]+|x))?/); let majorStr, minorStr, patchStr; if (verParts == null) { if (verStr[1] === 'x') { throw new ValidationError( 'The major version of a Flow version string cannot be `x`, it must ' + 'be a number!', ); } else { throw new ValidationError( 'Flow versions must be a non-range semver with an exact major version ' + 'and either an exact minor version or an `x` minor ver. Instead got: ' + verStr, ); } } else { majorStr = verParts[1]; minorStr = verParts[2]; patchStr = verParts[4]; } const [major, minor, patch] = [ _parseVerNum(majorStr, 'major', verStr), _parseVerNumOrX(minorStr, 'minor', verStr), _parseVerNumOrX(patchStr, 'patch', verStr), ]; const verAfterParts = verStr.substr(verParts[0].length + 1); if ( patchStr != null && verAfterParts[0] === '-' && verAfterParts[1] != null ) { if (expectPossibleRangeUpper) { // A `-` could indicate either a range or a prerel. This is technically // a real ambiguity in our versioning syntax -- but luckily it's rarely // encountered. // // To deal with this, we try parsing as a version. If it parses we assume // a range. If it doesn't parse as a version, we assume a pre-rel // identifier. // // This is excitingly inefficient but because it operates on tiny inputs // (and only sometimes) it shouldn't be an issue in practice. try { _parseVersion(verAfterParts.substr(1), false); return [verParts[0].length + 1, {major, minor, patch, prerel: null}]; } catch (e) { // It's possible that a prerel *and* a range co-exist! // v0.1.x-prerel-v0.2.x let prerelParts = verAfterParts.substr(1).split('-'); // ['prerel', 'v0.2.x'] let prerel = prerelParts.shift(); // 'prerel' while (prerelParts.length > 0) { try { _parseVersion(prerelParts.join('-'), false); break; } catch (e) { prerel += '-' + prerelParts.shift(); } } return [ verParts[0].length + '-'.length + prerel.length + 1, { major, minor, patch, prerel, }, ]; } } else { // After the `-` must be a prerel return [ verStr.length + 1, { major, minor, patch, prerel: verAfterParts.substr(1), }, ]; } } else { return [verParts[0].length + 1, {major, minor, patch, prerel: null}]; } } export function parseDirString(verStr: string): FlowVersion { const prefix = 'flow_'; if (!verStr.startsWith(prefix)) { throw new ValidationError( `Flow versions must start with \`${prefix}\` but instead got ${verStr}`, ); } const afterPrefix = verStr.substr(verStr.indexOf(prefix) + prefix.length); if (afterPrefix === 'all' || afterPrefix === 'vx.x.x') { return {kind: 'all'}; } else if (afterPrefix[0] === '-') { return { kind: 'ranged', lower: null, upper: _parseVersion(verStr.substr(`${prefix}-`.length), false)[1], }; } else { const [offset, lowerVer] = _parseVersion(afterPrefix, true); if (offset === afterPrefix.length) { return { kind: 'specific', ver: lowerVer, }; } else if (afterPrefix[offset] === '-') { const upperVer = offset + 1 === afterPrefix.length ? null : _parseVersion(afterPrefix.substr(offset + 1), false)[1]; return { kind: 'ranged', lower: lowerVer, upper: upperVer, }; } else { throw new ValidationError( `Unexpected trailing characters: ${afterPrefix.substr(offset)}`, ); } } } export function parseFlowSpecificVer(verStr: string): FlowSpecificVer { const flowVer = parseDirString(`flow_${verStr}`); switch (flowVer.kind) { case 'specific': return flowVer.ver; case 'all': case 'ranged': throw new ValidationError(`This is not a specific Flow version.`); default: (flowVer: empty); } return { major: -1, minor: 'x', patch: 'x', prerel: null, }; } export async function determineFlowSpecificVersion( cwd: string, flowVersionArg?: mixed, ): Promise<{| kind: 'specific', ver: FlowSpecificVer, |}> { if (flowVersionArg && typeof flowVersionArg === 'string') { // Be permissive if the prefix 'v' is left off let flowVersionStr = flowVersionArg[0] === 'v' ? flowVersionArg : `v${flowVersionArg}`; if (/^v[0-9]+\.[0-9]+$/.test(flowVersionStr)) { flowVersionStr = `${flowVersionStr}.0`; } return { kind: 'specific', ver: parseFlowSpecificVer(flowVersionStr), }; } else { return { kind: 'specific', ver: await findFlowSpecificVer(cwd), }; } } /** * Given two version ranges a and b, determine whether a is before b. */ function lt(n1: number | 'x', n2: number | 'x'): boolean | 'maybe' { if (n1 === 'x' || n2 === 'x') return false; if (n1 < n2) return true; if (n1 > n2) return false; return 'maybe'; } function before(a: FlowSpecificVer, b: FlowSpecificVer): boolean { let test = lt(a.major, b.major); if (test !== 'maybe') return test; test = lt(a.minor, b.minor); if (test !== 'maybe') return test; test = lt(a.patch, b.patch); if (test !== 'maybe') return test; return false; } /** * Given a version range, returns the max version satisfying the range. */ function maxSat(ver: FlowVersion): FlowSpecificVer | null { switch (ver.kind) { case 'all': return null; case 'ranged': return ver.upper; case 'specific': return ver.ver; default: (ver: empty); throw new Error('Unexpected FlowVersion kind!'); } } /** * Given a version range, returns the min version satisfying the range. */ function minSat(ver: FlowVersion): FlowSpecificVer | null { switch (ver.kind) { case 'all': return null; case 'ranged': return ver.lower; case 'specific': return ver.ver; default: (ver: empty); throw new Error('Unexpected FlowVersion kind!'); } } /** * Given two versions, returns whether they are disjoint. */ function _before(a: ?FlowSpecificVer, b: ?FlowSpecificVer): boolean { // If a is undefined, it denotes the maximum version. If b is undefined, it // denotes the minimum version. if (a && b) return before(a, b); return false; } function disjointVersions(a: FlowVersion, b: FlowVersion): boolean { return _before(maxSat(a), minSat(b)) || _before(maxSat(b), minSat(a)); } /** * Given an array of versions, returns whether they are mutually disjoint. */ function _disjointVersionsAll( vers: Array, len: number, i: number, ) { if (i + 1 >= len) return true; for (let j = i + 1; j < len; j++) { if (!disjointVersions(vers[i], vers[j])) { return false; } } return _disjointVersionsAll(vers, len, i + 1); } export function disjointVersionsAll(vers: Array): boolean { return _disjointVersionsAll(vers, vers.length, 0); } export function toDirString(ver: FlowVersion): string { switch (ver.kind) { case 'all': return 'flow_all'; case 'specific': { let str = `flow_v${ver.ver.major}.${ver.ver.minor}`; if (ver.ver.patch !== null) { str += `.${ver.ver.patch}`; if (ver.ver.prerel) { str += `-${ver.ver.prerel}`; } } return str; } case 'ranged': { const {lower, upper} = ver; let str = 'flow_'; if (lower !== null) { str += `v${lower.major}.${lower.minor}`; if (lower.patch !== null) { str += `.${lower.patch}`; if (lower.prerel !== null) { str += `-${lower.prerel}`; } } } str += '-'; if (upper !== null) { str += `v${upper.major}.${upper.minor}`; if (upper.patch !== null) { str += `.${upper.patch}`; if (upper.prerel !== null) { str += `-${upper.prerel}`; } } } return str; } default: (ver: empty); throw new Error('Unexpected FlowVersion kind!'); } } export function toSemverString(ver: FlowVersion): string { switch (ver.kind) { case 'all': return 'vx.x.x'; case 'specific': return toDirString(ver).substr('flow_'.length); case 'ranged': { const {upper, lower} = ver; let str = ''; if (lower !== null) { str += `>=v${lower.major}.${lower.minor}`; if (lower.patch !== null) { str += `.${lower.patch}`; if (lower.prerel !== null) { str += `-${lower.prerel}`; } } if (upper !== null) { str += ' '; } } if (upper !== null) { str += `<=v${upper.major}.${upper.minor}`; if (upper.patch !== null) { str += `.${upper.patch}`; if (upper.prerel !== null) { str += `-${upper.prerel}`; } } } return str; } default: (ver: empty); throw new Error('Unexpected FlowVersion kind!'); } } export function compareFlowVersionAsc(a: FlowVersion, b: FlowVersion): number { if (a.kind === 'all') { return b.kind === 'all' ? 0 : -1; } if (b.kind === 'all') { return 1; } const aLowerVer = a.kind === 'specific' ? a.ver : a.lower; const bLowerVer = b.kind === 'specific' ? b.ver : b.lower; if (aLowerVer === null) { return bLowerVer === null ? 0 : 1; } if (bLowerVer === null) { return -1; } const compareMajor = lt(aLowerVer.major, bLowerVer.major); if (compareMajor !== 'maybe') { return compareMajor ? 1 : -1; } const compareMinor = lt(aLowerVer.minor, bLowerVer.minor); if (compareMinor !== 'maybe') { return compareMinor ? 1 : -1; } const comparePatch = lt(aLowerVer.patch, bLowerVer.patch); if (comparePatch !== 'maybe') { return comparePatch ? 1 : -1; } return 0; } export const extractFlowDirFromFlowDirPath = (path: string): string => { const split = path.split('/'); return split[split.length - 1]; }; // Exported for tests export {_parseVersion as __parseVersion};