mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-10 14:06:30 +00:00
Merge 70069e21aa
into 6aa5c4238f
This commit is contained in:
commit
dd5500ea4e
10 changed files with 389 additions and 7 deletions
|
@ -12,6 +12,7 @@ export const Categories = [
|
|||
'dotnet',
|
||||
'elixir',
|
||||
'golang',
|
||||
'haskell',
|
||||
'helm',
|
||||
'iac',
|
||||
'java',
|
||||
|
|
|
@ -44,6 +44,7 @@ import * as gleam from './gleam';
|
|||
import * as gomod from './gomod';
|
||||
import * as gradle from './gradle';
|
||||
import * as gradleWrapper from './gradle-wrapper';
|
||||
import * as haskellCabal from './haskell-cabal';
|
||||
import * as helmRequirements from './helm-requirements';
|
||||
import * as helmValues from './helm-values';
|
||||
import * as helmfile from './helmfile';
|
||||
|
@ -150,6 +151,7 @@ api.set('gleam', gleam);
|
|||
api.set('gomod', gomod);
|
||||
api.set('gradle', gradle);
|
||||
api.set('gradle-wrapper', gradleWrapper);
|
||||
api.set('haskell-cabal', haskellCabal);
|
||||
api.set('helm-requirements', helmRequirements);
|
||||
api.set('helm-values', helmValues);
|
||||
api.set('helmfile', helmfile);
|
||||
|
|
86
lib/modules/manager/haskell-cabal/extract.spec.ts
Normal file
86
lib/modules/manager/haskell-cabal/extract.spec.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
countPackageNameLength,
|
||||
countPrecedingIndentation,
|
||||
extractNamesAndRanges,
|
||||
findExtents,
|
||||
splitSingleDependency,
|
||||
} from './extract';
|
||||
|
||||
describe('modules/manager/haskell-cabal/extract', () => {
|
||||
describe('countPackageNameLength', () => {
|
||||
it.each`
|
||||
input | expected
|
||||
${'-'} | ${null}
|
||||
${'-j'} | ${null}
|
||||
${'-H'} | ${null}
|
||||
${'j-'} | ${null}
|
||||
${'3-'} | ${null}
|
||||
${'-3'} | ${null}
|
||||
${'3'} | ${null}
|
||||
${'æ'} | ${null}
|
||||
${'æe'} | ${null}
|
||||
${'j'} | ${1}
|
||||
${'H'} | ${1}
|
||||
${'0ad'} | ${3}
|
||||
${'3d'} | ${2}
|
||||
`('matches $input', ({ input, expected }) => {
|
||||
const maybeIndex = countPackageNameLength(input);
|
||||
expect(maybeIndex).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('countPrecedingIndentation()', () => {
|
||||
it.each`
|
||||
content | index | expected
|
||||
${'\tbuild-depends: base\n\tother-field: hi'} | ${1} | ${1}
|
||||
${' build-depends: base'} | ${1} | ${1}
|
||||
${'a\tb'} | ${0} | ${0}
|
||||
${'a\tb'} | ${2} | ${1}
|
||||
${'a b'} | ${2} | ${1}
|
||||
${' b'} | ${2} | ${2}
|
||||
`(
|
||||
'countPrecedingIndentation($content, $index)',
|
||||
({ content, index, expected }) => {
|
||||
expect(countPrecedingIndentation(content, index)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('findExtents()', () => {
|
||||
it.each`
|
||||
content | indent | expected
|
||||
${'a: b\n\tc: d'} | ${1} | ${10}
|
||||
${'a: b'} | ${2} | ${4}
|
||||
${'a: b\n\tc: d'} | ${2} | ${4}
|
||||
${'a: b\n '} | ${2} | ${6}
|
||||
${'a: b\n c: d\ne: f'} | ${1} | ${10}
|
||||
`('findExtents($indent, $content)', ({ indent, content, expected }) => {
|
||||
expect(findExtents(indent, content)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitSingleDependency()', () => {
|
||||
it.each`
|
||||
depLine | expectedName | expectedRange
|
||||
${'base >=2 && <3'} | ${'base'} | ${'>=2 && <3'}
|
||||
${'base >=2 && <3 '} | ${'base'} | ${'>=2 && <3'}
|
||||
${'base>=2&&<3'} | ${'base'} | ${'>=2&&<3'}
|
||||
${'base'} | ${'base'} | ${''}
|
||||
`(
|
||||
'splitSingleDependency($depLine)',
|
||||
({ depLine, expectedName, expectedRange }) => {
|
||||
const res = splitSingleDependency(depLine);
|
||||
expect(res?.name).toBe(expectedName);
|
||||
expect(res?.range).toBe(expectedRange);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('extractNamesAndRanges()', () => {
|
||||
it('trims replaceString', () => {
|
||||
const res = extractNamesAndRanges(' a , b ');
|
||||
expect(res).toHaveLength(2);
|
||||
expect(res[0].replaceString).toBe('a');
|
||||
});
|
||||
});
|
||||
});
|
191
lib/modules/manager/haskell-cabal/extract.ts
Normal file
191
lib/modules/manager/haskell-cabal/extract.ts
Normal file
|
@ -0,0 +1,191 @@
|
|||
import { regEx } from '../../../util/regex';
|
||||
|
||||
const buildDependsRegex = regEx(
|
||||
/(?<buildDependsFieldName>build-depends[ \t]*:)/i,
|
||||
);
|
||||
function isNonASCII(str: string): boolean {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str.charCodeAt(i) > 127) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function countPackageNameLength(input: string): number | null {
|
||||
if (input.length < 1 || isNonASCII(input)) {
|
||||
return null;
|
||||
}
|
||||
if (!regEx(/^[A-Za-z0-9]/).test(input[0])) {
|
||||
// Must start with letter or number
|
||||
return null;
|
||||
}
|
||||
let idx = 1;
|
||||
while (idx < input.length) {
|
||||
if (regEx(/[A-Za-z0-9-]/).test(input[idx])) {
|
||||
idx++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!regEx(/[A-Za-z]/).test(input.slice(0, idx))) {
|
||||
// Must contain a letter
|
||||
return null;
|
||||
}
|
||||
if (idx - 1 < input.length && input[idx - 1] === '-') {
|
||||
// Can't end in a hyphen
|
||||
return null;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
export interface CabalDependency {
|
||||
packageName: string;
|
||||
currentValue: string;
|
||||
replaceString: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find extents of field contents
|
||||
*
|
||||
* @param {number} indent -
|
||||
* Indention level maintained within the block.
|
||||
* Any indention lower than this means it's outside the field.
|
||||
* Lines with this level or more are included in the field.
|
||||
* @returns {number}
|
||||
* Index just after the end of the block.
|
||||
* Note that it may be after the end of the string.
|
||||
*/
|
||||
export function findExtents(indent: number, content: string): number {
|
||||
let blockIdx: number = 0;
|
||||
let mode: 'finding-newline' | 'finding-indention' = 'finding-newline';
|
||||
for (;;) {
|
||||
if (mode === 'finding-newline') {
|
||||
while (content[blockIdx++] !== '\n') {
|
||||
if (blockIdx >= content.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (blockIdx >= content.length) {
|
||||
return content.length;
|
||||
}
|
||||
mode = 'finding-indention';
|
||||
} else {
|
||||
let thisIndent = 0;
|
||||
for (;;) {
|
||||
if ([' ', '\t'].includes(content[blockIdx])) {
|
||||
thisIndent += 1;
|
||||
blockIdx++;
|
||||
if (blockIdx >= content.length) {
|
||||
return content.length;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
mode = 'finding-newline';
|
||||
blockIdx++;
|
||||
break;
|
||||
}
|
||||
if (thisIndent < indent) {
|
||||
// go back to before the newline
|
||||
for (;;) {
|
||||
if (content[blockIdx--] === '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return blockIdx + 1;
|
||||
}
|
||||
mode = 'finding-newline';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find indention level of build-depends
|
||||
*
|
||||
* @param {number} match -
|
||||
* Search starts at this index, and proceeds backwards.
|
||||
* @returns {number}
|
||||
* Number of indention levels found before 'match'.
|
||||
*/
|
||||
export function countPrecedingIndentation(
|
||||
content: string,
|
||||
match: number,
|
||||
): number {
|
||||
let whitespaceIdx = match - 1;
|
||||
let indent = 0;
|
||||
while (whitespaceIdx >= 0 && [' ', '\t'].includes(content[whitespaceIdx])) {
|
||||
indent += 1;
|
||||
whitespaceIdx--;
|
||||
}
|
||||
return indent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find one 'build-depends' field name usage and its field value
|
||||
*
|
||||
* @returns {{buildDependsContent: string, lengthProcessed: number}}
|
||||
* buildDependsContent:
|
||||
* the contents of the field, excluding the field name and the colon.
|
||||
*
|
||||
* lengthProcessed:
|
||||
* points to after the end of the field. Note that the field does _not_
|
||||
* necessarily start at `content.length - lengthProcessed`.
|
||||
*
|
||||
* Returns null if no 'build-depends' field is found.
|
||||
*/
|
||||
export function findDepends(
|
||||
content: string,
|
||||
): { buildDependsContent: string; lengthProcessed: number } | null {
|
||||
const matchObj = buildDependsRegex.exec(content);
|
||||
if (matchObj === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const indent = countPrecedingIndentation(content, matchObj.index);
|
||||
const ourIdx: number =
|
||||
matchObj.index + matchObj.groups!['buildDependsFieldName'].length;
|
||||
const extent: number = findExtents(indent + 1, content.slice(ourIdx));
|
||||
return {
|
||||
buildDependsContent: content.slice(ourIdx, ourIdx + extent),
|
||||
lengthProcessed: ourIdx + extent,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a cabal single dependency into its constituent parts.
|
||||
* The first part is the package name, an optional second part contains
|
||||
* the version constraint.
|
||||
*
|
||||
* For example 'base == 3.2' would be split into 'base' and ' == 3.2'.
|
||||
*
|
||||
* @returns {{name: string, range: string}}
|
||||
* Null if the trimmed string doesn't begin with a package name.
|
||||
*/
|
||||
export function splitSingleDependency(
|
||||
input: string,
|
||||
): { name: string; range: string } | null {
|
||||
const match = countPackageNameLength(input);
|
||||
if (match === null) {
|
||||
return null;
|
||||
}
|
||||
const name: string = input.slice(0, match);
|
||||
const range = input.slice(match).trim();
|
||||
return { name, range };
|
||||
}
|
||||
|
||||
export function extractNamesAndRanges(content: string): CabalDependency[] {
|
||||
const list = content.split(',');
|
||||
const deps = [];
|
||||
for (const untrimmedReplaceString of list) {
|
||||
const replaceString = untrimmedReplaceString.trim();
|
||||
const maybeNameRange = splitSingleDependency(replaceString);
|
||||
if (maybeNameRange !== null) {
|
||||
deps.push({
|
||||
currentValue: maybeNameRange.range,
|
||||
packageName: maybeNameRange.name,
|
||||
replaceString,
|
||||
});
|
||||
}
|
||||
}
|
||||
return deps;
|
||||
}
|
33
lib/modules/manager/haskell-cabal/index.spec.ts
Normal file
33
lib/modules/manager/haskell-cabal/index.spec.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { extractPackageFile, getRangeStrategy } from '.';
|
||||
|
||||
describe('modules/manager/haskell-cabal/index', () => {
|
||||
describe('extractPackageFile()', () => {
|
||||
it.each`
|
||||
content | expected
|
||||
${'build-depends: base,'} | ${['base']}
|
||||
${'build-depends:,other,other2'} | ${['other', 'other2']}
|
||||
${'build-depends : base'} | ${['base']}
|
||||
${'Build-Depends: base'} | ${['base']}
|
||||
${'build-depends: a\nbuild-depends: b'} | ${['a', 'b']}
|
||||
${'dependencies: base'} | ${[]}
|
||||
`(
|
||||
'extractPackageFile($content).deps.map(x => x.packageName)',
|
||||
({ content, expected }) => {
|
||||
expect(
|
||||
extractPackageFile(content).deps.map((x) => x.packageName),
|
||||
).toStrictEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('getRangeStrategy()', () => {
|
||||
it.each`
|
||||
input | expected
|
||||
${'auto'} | ${'widen'}
|
||||
${'widen'} | ${'widen'}
|
||||
${'replace'} | ${'replace'}
|
||||
`('getRangeStrategy({ rangeStrategy: $input })', ({ input, expected }) => {
|
||||
expect(getRangeStrategy({ rangeStrategy: input })).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
58
lib/modules/manager/haskell-cabal/index.ts
Normal file
58
lib/modules/manager/haskell-cabal/index.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import type { Category } from '../../../constants';
|
||||
import type { RangeStrategy } from '../../../types';
|
||||
import { HackageDatasource } from '../../datasource/hackage';
|
||||
import * as pvpVersioning from '../../versioning/pvp';
|
||||
import type {
|
||||
PackageDependency,
|
||||
PackageFileContent,
|
||||
RangeConfig,
|
||||
} from '../types';
|
||||
import type { CabalDependency } from './extract';
|
||||
import { extractNamesAndRanges, findDepends } from './extract';
|
||||
|
||||
export const defaultConfig = {
|
||||
fileMatch: ['\\.cabal$'],
|
||||
pinDigests: false,
|
||||
versioning: pvpVersioning.id,
|
||||
};
|
||||
|
||||
export const categories: Category[] = ['haskell'];
|
||||
|
||||
export const supportedDatasources = [HackageDatasource.id];
|
||||
|
||||
export function extractPackageFile(content: string): PackageFileContent {
|
||||
const deps = [];
|
||||
let current = content;
|
||||
for (;;) {
|
||||
const maybeContent = findDepends(current);
|
||||
if (maybeContent === null) {
|
||||
break;
|
||||
}
|
||||
const cabalDeps: CabalDependency[] = extractNamesAndRanges(
|
||||
maybeContent.buildDependsContent,
|
||||
);
|
||||
for (const cabalDep of cabalDeps) {
|
||||
const dep: PackageDependency = {
|
||||
depName: cabalDep.packageName,
|
||||
currentValue: cabalDep.currentValue,
|
||||
datasource: HackageDatasource.id,
|
||||
packageName: cabalDep.packageName,
|
||||
versioning: 'pvp',
|
||||
replaceString: cabalDep.replaceString.trim(),
|
||||
autoReplaceStringTemplate: '{{{depName}}} {{{newValue}}}',
|
||||
};
|
||||
deps.push(dep);
|
||||
}
|
||||
current = current.slice(maybeContent.lengthProcessed);
|
||||
}
|
||||
return { deps };
|
||||
}
|
||||
|
||||
export function getRangeStrategy({
|
||||
rangeStrategy,
|
||||
}: RangeConfig): RangeStrategy {
|
||||
if (rangeStrategy === 'auto') {
|
||||
return 'widen';
|
||||
}
|
||||
return rangeStrategy;
|
||||
}
|
10
lib/modules/manager/haskell-cabal/readme.md
Normal file
10
lib/modules/manager/haskell-cabal/readme.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
Supports dependency extraction from `build-depends` fields in [Cabal package description files](https://cabal.readthedocs.io/en/3.12/cabal-package-description-file.html#pkg-field-build-depends).
|
||||
They use the extension `.cabal`, and are used with the [Haskell programming language](https://www.haskell.org/).
|
||||
|
||||
Limitations:
|
||||
|
||||
- The dependencies of all components are mushed together in one big list.
|
||||
- Fields like `pkgconfig-depends` and `build-tool-depends` are not handled.
|
||||
- The default PVP versioning is [subject to limitations](../../versioning/pvp/index.md).
|
||||
|
||||
If you need to change the versioning format, read the [versioning](../../versioning/index.md) documentation to learn more.
|
|
@ -141,12 +141,12 @@ describe('modules/versioning/pvp/index', () => {
|
|||
describe('.getNewValue(newValueConfig)', () => {
|
||||
it.each`
|
||||
currentValue | newVersion | rangeStrategy | expected
|
||||
${'>=1.0 && <1.1'} | ${'1.1'} | ${'auto'} | ${'>=1.0 && <1.2'}
|
||||
${'>=1.2 && <1.3'} | ${'1.2.3'} | ${'auto'} | ${null}
|
||||
${'>=1.0 && <1.1'} | ${'1.1'} | ${'widen'} | ${'>=1.0 && <1.2'}
|
||||
${'>=1.2 && <1.3'} | ${'1.2.3'} | ${'widen'} | ${null}
|
||||
${'>=1.0 && <1.1'} | ${'1.2.3'} | ${'update-lockfile'} | ${null}
|
||||
${'gibberish'} | ${'1.2.3'} | ${'auto'} | ${null}
|
||||
${'>=1.0 && <1.1'} | ${'0.9'} | ${'auto'} | ${null}
|
||||
${'>=1.0 && <1.1'} | ${''} | ${'auto'} | ${null}
|
||||
${'gibberish'} | ${'1.2.3'} | ${'widen'} | ${null}
|
||||
${'>=1.0 && <1.1'} | ${'0.9'} | ${'widen'} | ${null}
|
||||
${'>=1.0 && <1.1'} | ${''} | ${'widen'} | ${null}
|
||||
`(
|
||||
'pvp.getNewValue({currentValue: "$currentValue", newVersion: "$newVersion", rangeStrategy: "$rangeStrategy"}) === $expected',
|
||||
({ currentValue, newVersion, rangeStrategy, expected }) => {
|
||||
|
|
|
@ -9,7 +9,7 @@ export const id = 'pvp';
|
|||
export const displayName = 'Package Versioning Policy (Haskell)';
|
||||
export const urls = ['https://pvp.haskell.org'];
|
||||
export const supportsRanges = true;
|
||||
export const supportedRangeStrategies: RangeStrategy[] = ['auto'];
|
||||
export const supportedRangeStrategies: RangeStrategy[] = ['widen'];
|
||||
|
||||
const digitsAndDots = regEx(/^[\d.]+$/);
|
||||
|
||||
|
@ -112,7 +112,7 @@ function getNewValue({
|
|||
newVersion,
|
||||
rangeStrategy,
|
||||
}: NewValueConfig): string | null {
|
||||
if (rangeStrategy !== 'auto') {
|
||||
if (rangeStrategy !== 'widen') {
|
||||
logger.info(
|
||||
{ rangeStrategy, currentValue, newVersion },
|
||||
`PVP can't handle this range strategy.`,
|
||||
|
|
|
@ -48,6 +48,7 @@ export const CategoryNames: Record<Category, string> = {
|
|||
dotnet: '.NET',
|
||||
elixir: 'Elixir',
|
||||
golang: 'Go',
|
||||
haskell: 'Haskell',
|
||||
helm: 'Helm',
|
||||
iac: 'Infrastructure as Code',
|
||||
java: 'Java',
|
||||
|
|
Loading…
Reference in a new issue