diff --git a/lib/modules/manager/leiningen/extract.spec.ts b/lib/modules/manager/leiningen/extract.spec.ts index a5e92389bac9db..76e3d765e03d4e 100644 --- a/lib/modules/manager/leiningen/extract.spec.ts +++ b/lib/modules/manager/leiningen/extract.spec.ts @@ -1,3 +1,4 @@ +import { codeBlock } from 'common-tags'; import { Fixtures } from '../../../../test/fixtures'; import { ClojureDatasource } from '../../datasource/clojure'; import { extractFromVectors, extractVariables, trimAtKey } from './extract'; @@ -10,6 +11,12 @@ describe('modules/manager/leiningen/extract', () => { expect(trimAtKey('foo', 'bar')).toBeNull(); expect(trimAtKey(':dependencies ', 'dependencies')).toBeNull(); expect(trimAtKey(':dependencies \nfoobar', 'dependencies')).toBe('foobar'); + expect( + trimAtKey( + ':parent-project {:coords [my-org/my-parent "4.3.0"]\n:inherit [:profiles]}', + 'coords', + ), + ).toBe('[my-org/my-parent "4.3.0"]\n:inherit [:profiles]}'); }); it('extractFromVectors', () => { @@ -48,6 +55,20 @@ describe('modules/manager/leiningen/extract', () => { currentValue: '4.5.6', }, ]); + expect( + extractFromVectors( + '[my-org/my-parent "4.3.0"]\n:inherit [:profiles]}', + {}, + {}, + 1, + ), + ).toEqual([ + { + datasource: ClojureDatasource.id, + depName: 'my-org:my-parent', + currentValue: '4.3.0', + }, + ]); }); it('extractPackageFile', () => { @@ -124,6 +145,95 @@ describe('modules/manager/leiningen/extract', () => { }, ], }); + + const parentProjectSrc = codeBlock` +(defproject org.example/parent-project "1.0.0-SNAPSHOT" + :plugins [[lein-parent "0.3.9"] + [lein-project-version "0.1.0"] + [lein-shell "0.5.0"]] + :parent-project {:coords [my-org/my-parent "4.3.0"] + :inherit [:profiles :managed-dependencies :local-repo]} + :profiles {:cljfmt {:plugins [[lein-cljfmt "0.9.2"]]}} + :dependencies [[org.clojure/core.async "1.6.681"] + [org.clojure/core.match "1.1.0"] + [org.clojure/data.csv "1.1.0"] + [org.clojure/tools.cli "1.1.230"] + [metosin/malli "0.15.0"]])`; + + expect(extractPackageFile(parentProjectSrc)).toMatchObject({ + deps: [ + { + depName: 'org.clojure:core.async', + datasource: 'clojure', + depType: 'dependencies', + registryUrls: [], + currentValue: '1.6.681', + }, + { + depName: 'org.clojure:core.match', + datasource: 'clojure', + depType: 'dependencies', + registryUrls: [], + currentValue: '1.1.0', + }, + { + depName: 'org.clojure:data.csv', + datasource: 'clojure', + depType: 'dependencies', + registryUrls: [], + currentValue: '1.1.0', + }, + { + depName: 'org.clojure:tools.cli', + datasource: 'clojure', + depType: 'dependencies', + registryUrls: [], + currentValue: '1.1.230', + }, + { + depName: 'metosin:malli', + datasource: 'clojure', + depType: 'dependencies', + registryUrls: [], + currentValue: '0.15.0', + }, + { + depName: 'lein-parent:lein-parent', + datasource: 'clojure', + depType: 'plugins', + registryUrls: [], + currentValue: '0.3.9', + }, + { + depName: 'lein-project-version:lein-project-version', + datasource: 'clojure', + depType: 'plugins', + registryUrls: [], + currentValue: '0.1.0', + }, + { + depName: 'lein-shell:lein-shell', + datasource: 'clojure', + depType: 'plugins', + registryUrls: [], + currentValue: '0.5.0', + }, + { + depName: 'lein-cljfmt:lein-cljfmt', + datasource: 'clojure', + depType: 'plugins', + registryUrls: [], + currentValue: '0.9.2', + }, + { + depName: 'my-org:my-parent', + datasource: 'clojure', + depType: 'parent-project', + registryUrls: [], + currentValue: '4.3.0', + }, + ], + }); }); it('extractVariables', () => { diff --git a/lib/modules/manager/leiningen/extract.ts b/lib/modules/manager/leiningen/extract.ts index a6df1128927f8c..680b5f14cb025f 100644 --- a/lib/modules/manager/leiningen/extract.ts +++ b/lib/modules/manager/leiningen/extract.ts @@ -26,6 +26,7 @@ export function extractFromVectors( str: string, ctx: ExtractContext = {}, vars: ExtractedVariables = {}, + dimensions: 1 | 2 = 2, ): PackageDependency[] { if (!str.startsWith('[')) { return []; @@ -36,7 +37,8 @@ export function extractFromVectors( let vecPos = 0; let artifactId = ''; let version = ''; - let commentLevel = 0; + // Are we currently parsing a comment? If so, at what depth? + let commentLevel: number | null = null; const isSpace = (ch: string | null): boolean => !!ch && regEx(/[\s,]/).test(ch); @@ -82,7 +84,7 @@ export function extractFromVectors( if (char === '[') { balance += 1; - if (balance === 2) { + if (balance === dimensions) { vecPos = 0; } } else if (char === ']') { @@ -91,15 +93,17 @@ export function extractFromVectors( if (commentLevel === balance) { artifactId = ''; version = ''; - commentLevel = 0; + commentLevel = null; } - if (balance === 1) { + if (balance === dimensions - 1) { yieldDep(); - } else if (balance === 0) { + } + + if (balance === 0) { break; } - } else if (balance === 2) { + } else if (balance === dimensions) { if (isSpace(char)) { if (!isSpace(prevChar)) { vecPos += 1; @@ -170,20 +174,34 @@ export function extractVariables(content: string): ExtractedVariables { return result; } +interface CollectDepsOptions { + nested: boolean; + depType?: string; +} + function collectDeps( content: string, key: string, registryUrls: string[], vars: ExtractedVariables, + options: CollectDepsOptions = { + nested: true, + }, ): PackageDependency[] { const ctx = { - depType: key, + depType: options.depType ?? key, registryUrls, }; + // A vector like [["dep-1" "1.0.0"] ["dep-2" "0.0.0"]] is nested + // A vector like ["dep-1" "1.0.0"] is not + const dimensions = options.nested ? 2 : 1; let result: PackageDependency[] = []; let restContent = trimAtKey(content, key); while (restContent) { - result = [...result, ...extractFromVectors(restContent, ctx, vars)]; + result = [ + ...result, + ...extractFromVectors(restContent, ctx, vars, dimensions), + ]; restContent = trimAtKey(restContent, key); } return result; @@ -198,6 +216,19 @@ export function extractPackageFile(content: string): PackageFileContent { ...collectDeps(content, 'managed-dependencies', registryUrls, vars), ...collectDeps(content, 'plugins', registryUrls, vars), ...collectDeps(content, 'pom-plugins', registryUrls, vars), + // 'coords' is used in lein parent, and specifies zero or one + // dependencies. These are not wrapped in a vector in the way other + // dependencies are. The project.clj fragment looks like + // + // :parent-project {... :coords ["parent" "version"] ...} + // + // - https://github.com/achin/lein-parent + ...collectDeps(content, 'coords', registryUrls, vars, { + nested: false, + // The top-level key is 'parent-project', but we skip directly to 'coords'. + // So fix the dep type label + depType: 'parent-project', + }), ]; return { deps };