feat(swift): Support for Package.swift files (#3911)

This commit is contained in:
Sergio Zharinov 2019-06-24 20:43:48 +04:00 committed by Rhys Arkins
parent 1d6880b1a6
commit acd318a1d9
17 changed files with 2073 additions and 7 deletions

View file

@ -438,6 +438,7 @@ const options = [
'poetry',
'ruby',
'semver',
'swift',
],
default: 'semver',
cli: false,
@ -1811,6 +1812,19 @@ const options = [
env: false,
mergeable: true,
},
{
name: 'swift',
description: 'Configuration for Package.swift files',
stage: 'package',
type: 'object',
default: {
fileMatch: ['(^|/)Package\\.swift'],
versionScheme: 'swift',
rangeStrategy: 'bump',
},
mergeable: true,
cli: false,
},
];
function getOptions() {

View file

@ -6,7 +6,7 @@ const cacheMinutes = 10;
async function getPkgReleases({ lookupName }) {
try {
const cachedResult = await renovateCache.get(cacheNamespace, lookupName);
// istanbul ignore if
/* istanbul ignore next line */
if (cachedResult) return cachedResult;
const info = await getRemoteInfo({

View file

@ -27,6 +27,7 @@ const managerList = [
'poetry',
'pub',
'sbt',
'swift',
'terraform',
'travis',
'ruby-version',

View file

@ -0,0 +1,346 @@
const { isValid } = require('../../versioning/swift');
const regExps = {
wildcard: /^.*?/,
space: /(\s+|\/\/[^\n]*|\/\*.*\*\/)+/s,
depsKeyword: /dependencies/,
colon: /:/,
beginSection: /\[/,
endSection: /],?/,
package: /\s*.\s*package\s*\(\s*/,
urlKey: /url/,
stringLiteral: /"[^"]+"/,
comma: /,/,
from: /from/,
rangeOp: /\.\.[.<]/,
exactVersion: /\.\s*exact\s*\(\s*/,
};
const WILDCARD = 'wildcard';
const SPACE = 'space';
const DEPS = 'depsKeyword';
const COLON = 'colon';
const BEGIN_SECTION = 'beginSection';
const END_SECTION = 'endSection';
const PACKAGE = 'package';
const URL_KEY = 'urlKey';
const STRING_LITERAL = 'stringLiteral';
const COMMA = 'comma';
const FROM = 'from';
const RANGE_OP = 'rangeOp';
const EXACT_VERSION = 'exactVersion';
const searchLabels = {
wildcard: WILDCARD,
space: SPACE,
depsKeyword: DEPS,
colon: COLON,
beginSection: BEGIN_SECTION,
endSection: END_SECTION,
package: PACKAGE,
urlKey: URL_KEY,
stringLiteral: STRING_LITERAL,
comma: COMMA,
from: FROM,
rangeOp: RANGE_OP,
exactVersion: EXACT_VERSION,
};
function searchKeysForState(state) {
switch (state) {
case 'dependencies':
return [SPACE, COLON, WILDCARD];
case 'dependencies:':
return [SPACE, BEGIN_SECTION, WILDCARD];
case 'dependencies: [':
return [SPACE, PACKAGE, END_SECTION];
case '.package(':
return [SPACE, URL_KEY, PACKAGE, END_SECTION];
case '.package(url':
return [SPACE, COLON, PACKAGE, END_SECTION];
case '.package(url:':
return [SPACE, STRING_LITERAL, PACKAGE, END_SECTION];
case '.package(url: [depName]':
return [SPACE, COMMA, PACKAGE, END_SECTION];
case '.package(url: [depName],':
return [
SPACE,
FROM,
STRING_LITERAL,
RANGE_OP,
EXACT_VERSION,
PACKAGE,
END_SECTION,
];
case '.package(url: [depName], .exact(':
return [SPACE, STRING_LITERAL, PACKAGE, END_SECTION];
case '.package(url: [depName], from':
return [SPACE, COLON, PACKAGE, END_SECTION];
case '.package(url: [depName], from:':
return [SPACE, STRING_LITERAL, PACKAGE, END_SECTION];
case '.package(url: [depName], [value]':
return [SPACE, RANGE_OP, PACKAGE, END_SECTION];
case '.package(url: [depName], [rangeFrom][rangeOp]':
return [SPACE, STRING_LITERAL, PACKAGE, END_SECTION];
default:
return [DEPS];
}
}
function getMatch(str, state) {
const keys = searchKeysForState(state);
let result = null;
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const regex = regExps[key];
const label = searchLabels[key];
const match = str.match(regex);
if (match) {
const idx = match.index;
const substr = match[0];
const len = substr.length;
if (idx === 0) {
return { idx, len, label, substr };
}
if (!result || idx < result.idx) {
result = { idx, len, label, substr };
}
}
}
return result;
}
function getDepName(url) {
try {
const { host, pathname } = new URL(url);
if (host === 'github.com' || host === 'gitlab.com') {
return pathname
.replace(/^\//, '')
.replace(/\.git$/, '')
.replace(/\/$/, '');
}
return url;
} catch (e) {
return null;
}
}
function extractPackageFile(content, packageFile = null) {
if (!content) return null;
const result = {
packageFile,
};
const deps = [];
let offset = 0;
let restStr = content;
let state = null;
let match = getMatch(restStr, state);
let lookupName = null;
let currentValue = null;
let fileReplacePosition = null;
function yieldDep() {
const depName = getDepName(lookupName);
if (depName && currentValue && fileReplacePosition) {
const dep = {
datasource: 'gitTags',
depName,
lookupName,
currentValue,
fileReplacePosition,
};
if (isValid(currentValue)) {
deps.push(dep);
}
}
lookupName = null;
currentValue = null;
fileReplacePosition = null;
}
while (match) {
const { idx, len, label, substr } = match;
offset += idx;
// eslint-disable-next-line default-case
switch (state) {
case null:
if (deps.length) break;
if (label === DEPS) {
state = 'dependencies';
}
break;
case 'dependencies':
if (label === COLON) {
state = 'dependencies:';
} else if (label !== SPACE) {
state = null;
}
break;
case 'dependencies:':
if (label === BEGIN_SECTION) {
state = 'dependencies: [';
} else if (label !== SPACE) {
state = null;
}
break;
case 'dependencies: [':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === URL_KEY) {
state = '.package(url';
} else if (label === PACKAGE) {
yieldDep();
}
break;
case '.package(url':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === COLON) {
state = '.package(url:';
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(url:':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === STRING_LITERAL) {
lookupName = substr.replace(/^"/, '').replace(/"$/, '');
state = '.package(url: [depName]';
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(url: [depName]':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === COMMA) {
state = '.package(url: [depName],';
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(url: [depName],':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === FROM) {
fileReplacePosition = offset;
currentValue = substr;
state = '.package(url: [depName], from';
} else if (label === STRING_LITERAL) {
fileReplacePosition = offset;
currentValue = substr;
state = '.package(url: [depName], [value]';
} else if (label === RANGE_OP) {
fileReplacePosition = offset;
currentValue = substr;
state = '.package(url: [depName], [rangeFrom][rangeOp]';
} else if (label === EXACT_VERSION) {
state = '.package(url: [depName], .exact(';
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(url: [depName], .exact(':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === STRING_LITERAL) {
currentValue = substr.slice(1, substr.length - 1);
fileReplacePosition = offset;
yieldDep();
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(url: [depName], from':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === COLON) {
currentValue += substr;
state = '.package(url: [depName], from:';
} else if (label === SPACE) {
currentValue += substr;
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(url: [depName], from:':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === STRING_LITERAL) {
currentValue += substr;
yieldDep();
state = 'dependencies: [';
} else if (label === SPACE) {
currentValue += substr;
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(url: [depName], [value]':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === RANGE_OP) {
currentValue += substr;
state = '.package(url: [depName], [rangeFrom][rangeOp]';
} else if (label === SPACE) {
currentValue += substr;
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
case '.package(url: [depName], [rangeFrom][rangeOp]':
if (label === END_SECTION) {
yieldDep();
state = null;
} else if (label === STRING_LITERAL) {
currentValue += substr;
state = 'dependencies: [';
} else if (label === SPACE) {
currentValue += substr;
} else if (label === PACKAGE) {
yieldDep();
state = '.package(';
}
break;
}
offset += len;
restStr = restStr.slice(idx + len);
match = getMatch(restStr, state);
}
return deps.length ? { ...result, deps } : null;
}
module.exports = {
extractPackageFile,
};

View file

@ -0,0 +1,7 @@
const { extractPackageFile } = require('./extract');
const { updateDependency } = require('./update');
module.exports = {
extractPackageFile,
updateDependency,
};

View file

@ -0,0 +1,30 @@
const { isVersion } = require('../../versioning/swift');
const fromParam = /^\s*from\s*:\s*"([^"]+)"\s*$/;
function updateDependency(fileContent, upgrade) {
const { currentValue, newValue, fileReplacePosition } = upgrade;
const leftPart = fileContent.slice(0, fileReplacePosition);
const rightPart = fileContent.slice(fileReplacePosition);
const oldVal = isVersion(currentValue) ? `"${currentValue}"` : currentValue;
let newVal;
if (fromParam.test(oldVal)) {
const [, version] = oldVal.match(fromParam);
newVal = oldVal.replace(version, newValue);
} else if (isVersion(newValue)) {
newVal = `"${newValue}"`;
} else {
newVal = newValue;
}
if (rightPart.indexOf(oldVal) === 0) {
return leftPart + rightPart.replace(oldVal, newVal);
}
if (rightPart.indexOf(newVal) === 0) {
return fileContent;
}
return null;
}
module.exports = {
updateDependency,
};

View file

@ -0,0 +1,48 @@
const semver = require('semver');
const stable = require('semver-stable');
const { toSemverRange, getNewValue } = require('./range');
const { is: isStable } = stable;
const {
compare: sortVersions,
maxSatisfying,
minSatisfying,
major: getMajor,
minor: getMinor,
patch: getPatch,
satisfies,
valid,
validRange,
ltr,
gt: isGreaterThan,
eq: equals,
} = semver;
const isValid = input => !!valid(input) || !!validRange(toSemverRange(input));
const isVersion = input => !!valid(input);
const maxSatisfyingVersion = (versions, range) =>
maxSatisfying(versions, toSemverRange(range));
const minSatisfyingVersion = (versions, range) =>
minSatisfying(versions, toSemverRange(range));
const isLessThanRange = (version, range) => ltr(version, toSemverRange(range));
const matches = (version, range) => satisfies(version, toSemverRange(range));
module.exports = {
equals,
getMajor,
getMinor,
getNewValue,
getPatch,
isCompatible: isVersion,
isGreaterThan,
isLessThanRange,
isSingleVersion: isVersion,
isStable,
isValid,
isVersion,
matches,
maxSatisfyingVersion,
minSatisfyingVersion,
sortVersions,
};

View file

@ -0,0 +1,58 @@
const semver = require('semver');
const fromParam = /^\s*from\s*:\s*"([^"]+)"\s*$/;
const fromRange = /^\s*"([^"]+)"\s*\.\.\.\s*$/;
const binaryRange = /^\s*"([^"]+)"\s*(\.\.[.<])\s*"([^"]+)"\s*$/;
const toRange = /^\s*(\.\.[.<])\s*"([^"]+)"\s*$/;
function toSemverRange(range) {
if (fromParam.test(range)) {
const [, version] = range.match(fromParam);
if (semver.valid(version)) {
const nextMajor = `${semver.major(version) + 1}.0.0`;
return `>=${version} <${nextMajor}`;
}
} else if (fromRange.test(range)) {
const [, version] = range.match(fromRange);
if (semver.valid(version)) {
return `>=${version}`;
}
} else if (binaryRange.test(range)) {
const [, fromVersion, op, toVersion] = range.match(binaryRange);
if (semver.valid(fromVersion) && semver.valid(toVersion)) {
return op === '..<'
? `>=${fromVersion} <${toVersion}`
: `>=${fromVersion} <=${toVersion}`;
}
} else if (toRange.test(range)) {
const [, op, toVersion] = range.match(toRange);
if (semver.valid(toVersion)) {
return op === '..<' ? `<${toVersion}` : `<=${toVersion}`;
}
}
return null;
}
function getNewValue(currentValue, rangeStrategy, fromVersion, toVersion) {
if (fromParam.test(currentValue)) {
return toVersion;
}
if (fromRange.test(currentValue)) {
const [, version] = currentValue.match(fromRange);
return currentValue.replace(version, toVersion);
}
if (binaryRange.test(currentValue)) {
const [, , , version] = currentValue.match(binaryRange);
return currentValue.replace(version, toVersion);
}
if (toRange.test(currentValue)) {
const [, , version] = currentValue.match(toRange);
return currentValue.replace(version, toVersion);
}
return currentValue;
}
module.exports = {
toSemverRange,
getNewValue,
};

View file

@ -290,7 +290,8 @@
"pep440",
"poetry",
"ruby",
"semver"
"semver",
"swift"
],
"default": "semver"
},
@ -1241,6 +1242,16 @@
"description": "Options to suppress various types of warnings and other notifications",
"type": "array",
"default": ["deprecationWarningIssues"]
},
"swift": {
"description": "Configuration for Package.swift files",
"type": "object",
"default": {
"fileMatch": ["(^|/)Package\\.swift"],
"versionScheme": "swift",
"rangeStrategy": "bump"
},
"$ref": "#"
}
}
}

View file

@ -87,7 +87,7 @@ Array [
"depName": "Configuration Error",
"message": "packageRules:
You have included an unsupported manager in a package rule. Your list: foo.
Supported managers are: (ansible, bazel, buildkite, bundler, cargo, circleci, composer, deps-edn, docker-compose, dockerfile, github-actions, gitlabci, gomod, gradle, gradle-wrapper, kubernetes, leiningen, maven, meteor, npm, nuget, nvm, pip_requirements, pip_setup, pipenv, poetry, pub, sbt, terraform, travis, ruby-version, homebrew).",
Supported managers are: (ansible, bazel, buildkite, bundler, cargo, circleci, composer, deps-edn, docker-compose, dockerfile, github-actions, gitlabci, gomod, gradle, gradle-wrapper, kubernetes, leiningen, maven, meteor, npm, nuget, nvm, pip_requirements, pip_setup, pipenv, poetry, pub, sbt, swift, terraform, travis, ruby-version, homebrew).",
},
]
`;

View file

@ -3,21 +3,23 @@ const { getPkgReleases } = require('../../lib/datasource/git-tags');
jest.mock('isomorphic-git');
const lookupName = 'https://github.com/vapor/vapor.git';
const lookupName = 'vapor';
const registryUrls = ['https://github.com/vapor/vapor.git'];
const registryUrlsAlt = ['https://github.com/vapor/vapor/'];
describe('datasource/git-tags', () => {
beforeEach(() => global.renovateCache.rmAll());
describe('getPkgReleases', () => {
it('returns nil if response is wrong', async () => {
getRemoteInfo.mockReturnValue(Promise.resolve(null));
const versions = await getPkgReleases({ lookupName });
const versions = await getPkgReleases({ lookupName, registryUrls });
expect(versions).toEqual(null);
});
it('returns nil if remote call throws exception', async () => {
getRemoteInfo.mockImplementation(() => {
throw new Error();
});
const versions = await getPkgReleases({ lookupName });
const versions = await getPkgReleases({ lookupName, registryUrls });
expect(versions).toEqual(null);
});
it('returns versions filtered from tags', async () => {
@ -32,7 +34,10 @@ describe('datasource/git-tags', () => {
},
})
);
const versions = await getPkgReleases({ lookupName });
const versions = await getPkgReleases({
lookupName,
registryUrls: registryUrlsAlt,
});
const result = versions.releases.map(x => x.version).sort();
expect(result).toEqual(['0.0.1', '0.0.2']);
});

View file

@ -0,0 +1,134 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/manager/swift extractPackageFile() parses multiple packages 1`] = `
Object {
"deps": Array [
Object {
"currentValue": "0.1.0",
"datasource": "gitTags",
"depName": "avito-tech/GraphiteClient",
"fileReplacePosition": 1177,
"lookupName": "https://github.com/avito-tech/GraphiteClient.git",
},
Object {
"currentValue": "1.0.16",
"datasource": "gitTags",
"depName": "IBM-Swift/BlueSignals",
"fileReplacePosition": 1268,
"lookupName": "https://github.com/IBM-Swift/BlueSignals.git",
},
Object {
"currentValue": "3.0.6",
"datasource": "gitTags",
"depName": "daltoniam/Starscream",
"fileReplacePosition": 1439,
"lookupName": "https://github.com/daltoniam/Starscream.git",
},
Object {
"currentValue": "1.4.6",
"datasource": "gitTags",
"depName": "httpswift/swifter",
"fileReplacePosition": 1523,
"lookupName": "https://github.com/httpswift/swifter.git",
},
Object {
"currentValue": "from : \\"0.9.6\\"",
"datasource": "gitTags",
"depName": "weichsel/ZIPFoundation",
"fileReplacePosition": 1626,
"lookupName": "https://github.com/weichsel/ZIPFoundation/",
},
],
"packageFile": null,
}
`;
exports[`lib/manager/swift extractPackageFile() parses package descriptions 1`] = `
Object {
"deps": Array [
Object {
"currentValue": "from:\\"1.2.3\\"",
"datasource": "gitTags",
"depName": "vapor/vapor",
"fileReplacePosition": 64,
"lookupName": "https://github.com/vapor/vapor.git",
},
],
"packageFile": null,
}
`;
exports[`lib/manager/swift extractPackageFile() parses package descriptions 2`] = `
Object {
"deps": Array [
Object {
"currentValue": "\\"1.2.3\\"...",
"datasource": "gitTags",
"depName": "vapor/vapor",
"fileReplacePosition": 64,
"lookupName": "https://github.com/vapor/vapor.git",
},
],
"packageFile": null,
}
`;
exports[`lib/manager/swift extractPackageFile() parses package descriptions 3`] = `
Object {
"deps": Array [
Object {
"currentValue": "\\"1.2.3\\"...\\"1.2.4\\"",
"datasource": "gitTags",
"depName": "vapor/vapor",
"fileReplacePosition": 64,
"lookupName": "https://github.com/vapor/vapor.git",
},
],
"packageFile": null,
}
`;
exports[`lib/manager/swift extractPackageFile() parses package descriptions 4`] = `
Object {
"deps": Array [
Object {
"currentValue": "\\"1.2.3\\"..<\\"1.2.4\\"",
"datasource": "gitTags",
"depName": "vapor/vapor",
"fileReplacePosition": 64,
"lookupName": "https://github.com/vapor/vapor.git",
},
],
"packageFile": null,
}
`;
exports[`lib/manager/swift extractPackageFile() parses package descriptions 5`] = `
Object {
"deps": Array [
Object {
"currentValue": "...\\"1.2.3\\"",
"datasource": "gitTags",
"depName": "vapor/vapor",
"fileReplacePosition": 64,
"lookupName": "https://github.com/vapor/vapor.git",
},
],
"packageFile": null,
}
`;
exports[`lib/manager/swift extractPackageFile() parses package descriptions 6`] = `
Object {
"deps": Array [
Object {
"currentValue": "..<\\"1.2.3\\"",
"datasource": "gitTags",
"depName": "vapor/vapor",
"fileReplacePosition": 64,
"lookupName": "https://github.com/vapor/vapor.git",
},
],
"packageFile": null,
}
`;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,203 @@
const fs = require('fs');
const path = require('path');
const { extractPackageFile } = require('../../../lib/manager/swift/extract');
const { updateDependency } = require('../../../lib/manager/swift/update');
const pkgContent = fs.readFileSync(
path.resolve(__dirname, `./_fixtures/SamplePackage.swift`),
'utf8'
);
describe('lib/manager/swift', () => {
describe('extractPackageFile()', () => {
it('returns null for empty content', () => {
expect(extractPackageFile(null)).toBeNull();
expect(extractPackageFile(``)).toBeNull();
expect(extractPackageFile(`dependencies:[]`)).toBeNull();
expect(extractPackageFile(`dependencies:["foobar"]`)).toBeNull();
});
it('returns null for invalid content', () => {
expect(extractPackageFile(`dependen`)).toBeNull();
expect(extractPackageFile(`dependencies!: `)).toBeNull();
expect(extractPackageFile(`dependencies :`)).toBeNull();
expect(extractPackageFile(`dependencies...`)).toBeNull();
expect(extractPackageFile(`dependencies:!`)).toBeNull();
expect(extractPackageFile(`dependencies:[`)).toBeNull();
expect(extractPackageFile(`dependencies:[...`)).toBeNull();
expect(extractPackageFile(`dependencies:[]`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package.package(`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package(asdf`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package]`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package(]`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package(.package(`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package(`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package(]`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package(url],`)).toBeNull();
expect(
extractPackageFile(`dependencies:[.package(url.package(]`)
).toBeNull();
expect(
extractPackageFile(`dependencies:[.package(url:.package(`)
).toBeNull();
expect(extractPackageFile(`dependencies:[.package(url:]`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package(url:"fo`)).toBeNull();
expect(extractPackageFile(`dependencies:[.package(url:"fo]`)).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://example.com/something.git"]`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git"]`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git".package(]`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git", ]`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git", .package(]`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git", .exact(]`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git", from]`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git", from.package(`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git", from:]`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git", from:.package(`
)
).toBeNull();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git","1.2.3")]`
)
).toBeNull();
});
it('parses package descriptions', () => {
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git",from:"1.2.3")]`
)
).toMatchSnapshot();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git","1.2.3"...)]`
)
).toMatchSnapshot();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git","1.2.3"..."1.2.4")]`
)
).toMatchSnapshot();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git","1.2.3"..<"1.2.4")]`
)
).toMatchSnapshot();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git",..."1.2.3")]`
)
).toMatchSnapshot();
expect(
extractPackageFile(
`dependencies:[.package(url:"https://github.com/vapor/vapor.git",..<"1.2.3")]`
)
).toMatchSnapshot();
});
it('parses multiple packages', () => {
expect(extractPackageFile(pkgContent)).toMatchSnapshot();
});
});
describe('updateDependency()', () => {
it('updates successfully', () => {
[
[
'dependencies:[.package(url:"https://github.com/vapor/vapor.git",.exact("1.2.3")]',
'1.2.4',
'dependencies:[.package(url:"https://github.com/vapor/vapor.git",.exact("1.2.4")]',
],
[
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", from: "1.2.3")]',
'1.2.4',
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", from: "1.2.4")]',
],
[
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", "1.2.3"..."1.2.4")]',
'"1.2.3"..."1.2.5"',
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", "1.2.3"..."1.2.5")]',
],
[
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", "1.2.3"..<"1.2.4")]',
'"1.2.3"..<"1.2.5"',
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", "1.2.3"..<"1.2.5")]',
],
[
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", ..."1.2.4")]',
'..."1.2.5"',
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", ..."1.2.5")]',
],
[
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", ..<"1.2.4")]',
'..<"1.2.5"',
'dependencies:[.package(url:"https://github.com/vapor/vapor.git", ..<"1.2.5")]',
],
].forEach(([content, newValue, result]) => {
const { deps } = extractPackageFile(content);
const [dep] = deps;
const upgrade = { ...dep, newValue };
const updated = updateDependency(content, upgrade);
expect(updated).toEqual(result);
});
});
it('returns content if already updated', () => {
const content =
'dependencies:[.package(url:"https://github.com/vapor/vapor.git",.exact("1.2.3")]';
const currentValue = '1.2.3';
const newValue = '1.2.4';
const { deps } = extractPackageFile(content);
const [dep] = deps;
const upgrade = { ...dep, newValue };
const replaced = content.replace(currentValue, newValue);
const updated = updateDependency(replaced, upgrade);
expect(updated).toBe(replaced);
});
it('returns null if content is different', () => {
const content =
'dependencies:[.package(url:"https://github.com/vapor/vapor.git",.exact("1.2.3")]';
const currentValue = '1.2.3';
const newValue = '1.2.4';
const { deps } = extractPackageFile(content);
const [dep] = deps;
const upgrade = { ...dep, newValue };
const replaced = content.replace(currentValue, '1.2.5');
expect(updateDependency(replaced, upgrade)).toBe(null);
});
});
});

View file

@ -0,0 +1,85 @@
const {
getNewValue,
isValid,
minSatisfyingVersion,
maxSatisfyingVersion,
isLessThanRange,
matches,
} = require('../../lib/versioning/swift');
describe('isValid(input)', () => {
it('understands Swift version ranges', () => {
expect(isValid('from: "1.2.3"')).toBe(true);
expect(isValid('from : "1.2.3"')).toBe(true);
expect(isValid('from:"1.2.3"')).toBe(true);
expect(isValid(' from:"1.2.3" ')).toBe(true);
expect(isValid(' from : "1.2.3" ')).toBe(true);
expect(isValid('"1.2.3"..."1.2.4"')).toBe(true);
expect(isValid(' "1.2.3" ... "1.2.4" ')).toBe(true);
expect(isValid('"1.2.3"...')).toBe(true);
expect(isValid(' "1.2.3" ... ')).toBe(true);
expect(isValid('..."1.2.4"')).toBe(true);
expect(isValid(' ... "1.2.4" ')).toBe(true);
expect(isValid('"1.2.3"..<"1.2.4"')).toBe(true);
expect(isValid(' "1.2.3" ..< "1.2.4" ')).toBe(true);
expect(isValid('..<"1.2.4"')).toBe(true);
expect(isValid(' ..< "1.2.4" ')).toBe(true);
});
it('should return null for irregular versions', () => {
expect(isValid('17.04.0')).toBeFalsy();
});
it('should support simple semver', () => {
expect(isValid('1.2.3')).toBe(true);
});
it('should support semver with dash', () => {
expect(isValid('1.2.3-foo')).toBe(true);
});
it('should reject semver without dash', () => {
expect(isValid('1.2.3foo')).toBeFalsy();
});
it('should support ranges', () => {
expect(isValid('~1.2.3')).toBeFalsy();
expect(isValid('^1.2.3')).toBeFalsy();
expect(isValid('from: "1.2.3"')).toBe(true);
expect(isValid('"1.2.3"..."1.2.4"')).toBe(true);
expect(isValid('"1.2.3"..."1.2.4"')).toBe(true);
expect(isValid('"1.2.3"..<"1.2.4"')).toBe(true);
expect(isValid('"1.2.3"..<"1.2.4"')).toBe(true);
expect(isValid('..."1.2.3"')).toBe(true);
expect(isValid('..<"1.2.4"')).toBe(true);
expect(
minSatisfyingVersion(['1.2.3', '1.2.4', '1.2.5'], '..<"1.2.4"')
).toBe('1.2.3');
expect(
maxSatisfyingVersion(['1.2.3', '1.2.4', '1.2.5'], '..<"1.2.4"')
).toBe('1.2.3');
expect(
maxSatisfyingVersion(['1.2.3', '1.2.4', '1.2.5'], '..."1.2.4"')
).toBe('1.2.4');
expect(isLessThanRange('1.2.3', '..."1.2.4"')).toBe(false);
expect(isLessThanRange('1.2.3', '"1.2.4"...')).toBe(true);
expect(matches('1.2.4', '..."1.2.4"')).toBe(true);
expect(matches('1.2.4', '..."1.2.3"')).toBe(false);
});
});
describe('getNewValue()', () => {
it('supports range update', () => {
[
['1.2.3', 'auto', '1.2.3', '1.2.4', '1.2.3'],
['from: "1.2.3"', 'auto', '1.2.3', '1.2.4', '1.2.4'],
['"1.2.3"...', 'auto', '1.2.3', '1.2.4', '"1.2.4"...'],
['"1.2.3"..."1.2.4"', 'auto', '1.2.3', '1.2.5', '"1.2.3"..."1.2.5"'],
['"1.2.3"..<"1.2.4"', 'auto', '1.2.3', '1.2.5', '"1.2.3"..<"1.2.5"'],
['..."1.2.4"', 'auto', '1.2.3', '1.2.5', '..."1.2.5"'],
['..<"1.2.4"', 'auto', '1.2.3', '1.2.5', '..<"1.2.5"'],
].forEach(([range, strategy, fromVersion, toVersion, result]) => {
const newValue = getNewValue(range, strategy, fromVersion, toVersion);
expect(newValue).toEqual(result);
});
});
});

View file

@ -92,6 +92,9 @@ Object {
"sbt": Array [
Object {},
],
"swift": Array [
Object {},
],
"terraform": Array [
Object {},
],

View file

@ -1080,6 +1080,22 @@ Use this field to suppress various types of warnings and other notifications fro
The above config will suppress the comment which is added to a PR whenever you close a PR unmerged.
## swift
Anything other than `.exact(<...>)` will be treated as range with respect to Swift specific.
Because of this, some PR descriptions will look like `from: <...> => <...>`.
Examples:
```swift
package(name: "<...>", from: "1.2.3") // => from: "2.0.0"
package(name: "<...>", "1.2.3"...) // => "2.0.0"...
package(name: "<...>", "1.2.3"..."1.3.0") // => "1.2.3"..."2.0.0"
package(name: "<...>", "1.2.3"..<"1.3.0") // => "1.2.3"..<"2.0.0"
package(name: "<...>", ..."1.2.3") // => ..."2.0.0"
package(name: "<...>", ..<"1.2.3") // => ..<"2.0.0"
```
## terraform
Currently Terraform support is limited to Terraform registry sources and github sources that include semver refs, e.g. like `github.com/hashicorp/example?ref=v1.0.0`.