diff --git a/lib/modules/datasource/clojure/common.ts b/lib/modules/datasource/clojure/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee02fa6e0e7ec85b76527777624d1810650b5c09 --- /dev/null +++ b/lib/modules/datasource/clojure/common.ts @@ -0,0 +1 @@ +export const CLOJARS_REPO = 'https://clojars.org/repo'; diff --git a/lib/modules/datasource/clojure/index.ts b/lib/modules/datasource/clojure/index.ts index 10a1426b4ea2365cab5f40fa5c6b59dfea190986..c070559b922f10fbe658e136984b0fd554a4dd63 100644 --- a/lib/modules/datasource/clojure/index.ts +++ b/lib/modules/datasource/clojure/index.ts @@ -1,5 +1,6 @@ import { MavenDatasource } from '../maven'; import { MAVEN_REPO } from '../maven/common'; +import { CLOJARS_REPO } from './common'; export class ClojureDatasource extends MavenDatasource { static override readonly id = 'clojure'; @@ -10,8 +11,5 @@ export class ClojureDatasource extends MavenDatasource { override readonly registryStrategy = 'merge'; - override readonly defaultRegistryUrls = [ - 'https://clojars.org/repo', - MAVEN_REPO, - ]; + override readonly defaultRegistryUrls = [CLOJARS_REPO, MAVEN_REPO]; } diff --git a/lib/modules/manager/deps-edn/__fixtures__/deps.edn b/lib/modules/manager/deps-edn/__fixtures__/deps.edn index 5c94cc82bf9f3a68e8afa4597fe29e45cd109f9b..c1c3a2b5ea86cd6493525a615ce057f6f8a1dbe5 100644 --- a/lib/modules/manager/deps-edn/__fixtures__/deps.edn +++ b/lib/modules/manager/deps-edn/__fixtures__/deps.edn @@ -1,6 +1,8 @@ { :deps { ,,,,persistent-sorted-set,{:mvn/version,"0.1.2"} + invalid/package! {:mvn/version "1.2.3"} + invalid/version nil } :aliases { @@ -26,8 +28,36 @@ :extra-paths ["test"] :extra-deps { org.clojure/clojurescript {:mvn/version "1.10.520"} - lambdaisland/kaocha {:mvn/version "0.0-389"} - lambdaisland/kaocha-cljs {:mvn/version "0.0-21"} + lambdaisland/kaocha {:git/url "https://github.com/lambdaisland/kaocha.git" + :git/tag "0.0-389"} + io.github.lambdaisland/kaocha-cljs {:git/tag "0.0-21"} + } + } + + :test-gitlab { + :extra-paths ["test"] + :extra-deps { + lambdaisland/kaocha {:git/url "https://gitlab.com/lambdaisland/kaocha.git" + :git/tag "0.0-389"} + com.gitlab.lambdaisland/kaocha-cljs {:git/tag "0.0-21"} + } + } + + :test-bitbucket { + :extra-paths ["test"] + :extra-deps { + lambdaisland/kaocha {:git/url "https://bitbucket.org/lambdaisland/kaocha.git" + :git/tag "0.0-389"} + org.bitbucket.lambdaisland/kaocha-cljs {:git/tag "0.0-21"} + } + } + + :test-git { + :extra-paths ["test"] + :extra-deps { + foo/foo {:git/url "git@example.com/foo.git" :git/sha "123"} + bar/bar {:git/url "https://example.com/bar.git"} + baz/baz {} } } @@ -48,4 +78,11 @@ } } } + + :mvn/repos { + "my-auth-repo" {:url "https://my.auth.com/repo"} + "central" nil + "my-private-repo" {:url "s3://my-bucket/maven/releases"} + "clojars" {:url "https://deps.com/foo/bar"} + } } diff --git a/lib/modules/manager/deps-edn/__snapshots__/extract.spec.ts.snap b/lib/modules/manager/deps-edn/__snapshots__/extract.spec.ts.snap index 36da072819f5e94550186f7ce306040baf25524e..7b7a98f13b781355e9ece944acb0ca06ddbb01e7 100644 --- a/lib/modules/manager/deps-edn/__snapshots__/extract.spec.ts.snap +++ b/lib/modules/manager/deps-edn/__snapshots__/extract.spec.ts.snap @@ -1,78 +1,208 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`modules/manager/deps-edn/extract extractPackageFile 1`] = ` +exports[`modules/manager/deps-edn/extract extractPackageFile extractPackageFile 1`] = ` Array [ Object { "currentValue": "0.1.2", "datasource": "clojure", - "depName": "persistent-sorted-set:persistent-sorted-set", - "registryUrls": Array [], + "depName": "persistent-sorted-set", + "packageName": "persistent-sorted-set:persistent-sorted-set", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version,\\"0.1.2\\"}", }, Object { "currentValue": "1.9.0", "datasource": "clojure", - "depName": "org.clojure:clojure", - "registryUrls": Array [], + "depName": "org.clojure/clojure", + "depType": "1.9", + "packageName": "org.clojure:clojure", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"1.9.0\\"}", }, Object { "currentValue": "1.10.0", "datasource": "clojure", - "depName": "org.clojure:clojure", - "registryUrls": Array [], + "depName": "org.clojure/clojure", + "depType": "1.10", + "packageName": "org.clojure:clojure", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"1.10.0\\"}", }, Object { "currentValue": "1.10.520", "datasource": "clojure", - "depName": "org.clojure:clojurescript", - "registryUrls": Array [], + "depName": "org.clojure/clojurescript", + "depType": "dev", + "packageName": "org.clojure:clojurescript", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"1.10.520\\"}", }, Object { "currentValue": "0.2.11", "datasource": "clojure", - "depName": "org.clojure:tools.namespace", - "registryUrls": Array [], + "depName": "org.clojure/tools.namespace", + "depType": "dev", + "packageName": "org.clojure:tools.namespace", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"0.2.11\\"}", }, Object { "currentValue": "1.10.520", "datasource": "clojure", - "depName": "org.clojure:clojurescript", - "registryUrls": Array [], + "depName": "org.clojure/clojurescript", + "depType": "test", + "packageName": "org.clojure:clojurescript", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"1.10.520\\"}", }, Object { "currentValue": "0.0-389", - "datasource": "clojure", - "depName": "lambdaisland:kaocha", - "registryUrls": Array [], + "datasource": "github-tags", + "depName": "lambdaisland/kaocha", + "depType": "test", + "packageName": "lambdaisland/kaocha", + "replaceString": "{:git/url \\"https://github.com/lambdaisland/kaocha.git\\" + :git/tag \\"0.0-389\\"}", + "sourceUrl": "https://github.com/lambdaisland/kaocha", }, Object { "currentValue": "0.0-21", - "datasource": "clojure", - "depName": "lambdaisland:kaocha-cljs", - "registryUrls": Array [], + "datasource": "github-tags", + "depName": "io.github.lambdaisland/kaocha-cljs", + "depType": "test", + "packageName": "lambdaisland/kaocha-cljs", + "replaceString": "{:git/tag \\"0.0-21\\"}", + }, + Object { + "currentValue": "0.0-389", + "datasource": "gitlab-tags", + "depName": "lambdaisland/kaocha", + "depType": "test-gitlab", + "packageName": "lambdaisland/kaocha", + "replaceString": "{:git/url \\"https://gitlab.com/lambdaisland/kaocha.git\\" + :git/tag \\"0.0-389\\"}", + "sourceUrl": "https://gitlab.com/lambdaisland/kaocha", + }, + Object { + "currentValue": "0.0-21", + "datasource": "gitlab-tags", + "depName": "com.gitlab.lambdaisland/kaocha-cljs", + "depType": "test-gitlab", + "packageName": "lambdaisland/kaocha-cljs", + "replaceString": "{:git/tag \\"0.0-21\\"}", + }, + Object { + "currentValue": "0.0-389", + "datasource": "gitlab-tags", + "depName": "lambdaisland/kaocha", + "depType": "test-bitbucket", + "packageName": "lambdaisland/kaocha", + "replaceString": "{:git/url \\"https://bitbucket.org/lambdaisland/kaocha.git\\" + :git/tag \\"0.0-389\\"}", + "sourceUrl": "https://bitbucket.org/lambdaisland/kaocha", + }, + Object { + "currentValue": "0.0-21", + "datasource": "bitbucket-tags", + "depName": "org.bitbucket.lambdaisland/kaocha-cljs", + "depType": "test-bitbucket", + "packageName": "lambdaisland/kaocha-cljs", + "replaceString": "{:git/tag \\"0.0-21\\"}", + }, + Object { + "currentDigest": "123", + "currentDigestShort": "123", + "currentValue": null, + "datasource": "git-refs", + "depName": "foo/foo", + "depType": "test-git", + "packageName": "git@example.com/foo.git", + "replaceString": "{:git/url \\"git@example.com/foo.git\\" :git/sha \\"123\\"}", + }, + Object { + "currentValue": null, + "datasource": "git-refs", + "depName": "bar/bar", + "depType": "test-git", + "packageName": "https://example.com/bar.git", + "replaceString": "{:git/url \\"https://example.com/bar.git\\"}", + "sourceUrl": "https://example.com/bar", }, Object { "currentValue": "0.21.1", "datasource": "clojure", - "depName": "cider:cider-nrepl", - "registryUrls": Array [], + "depName": "cider/cider-nrepl", + "depType": "repl", + "packageName": "cider:cider-nrepl", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"0.21.1\\"}", }, Object { "currentValue": "0.6.0", "datasource": "clojure", - "depName": "nrepl:nrepl", - "registryUrls": Array [], + "depName": "nrepl/nrepl", + "depType": "repl", + "packageName": "nrepl:nrepl", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"0.6.0\\"}", }, Object { "currentValue": "0.2.11", "datasource": "clojure", - "depName": "org.clojure:tools.namespace", - "registryUrls": Array [], + "depName": "org.clojure/tools.namespace", + "depType": "repl", + "packageName": "org.clojure:tools.namespace", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"0.2.11\\"}", }, Object { "currentValue": "0.9.5703", "datasource": "clojure", - "depName": "com.datomic:datomic-free", - "registryUrls": Array [], + "depName": "com.datomic/datomic-free", + "depType": "datomic", + "packageName": "com.datomic:datomic-free", + "registryUrls": Array [ + "https://deps.com/foo/bar", + "https://my.auth.com/repo", + "s3://my-bucket/maven/releases", + ], + "replaceString": "{:mvn/version \\"0.9.5703\\"}", }, ] `; diff --git a/lib/modules/manager/deps-edn/__snapshots__/parser.spec.ts.snap b/lib/modules/manager/deps-edn/__snapshots__/parser.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..5c9e49ed0859c3a3afa1a4321da88c945720640a --- /dev/null +++ b/lib/modules/manager/deps-edn/__snapshots__/parser.spec.ts.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`modules/manager/deps-edn/parser parseEdnFile extracts file 1`] = ` +Object { + "aliases": Object { + "1.10": Object { + "override-deps": Object { + "org.clojure/clojure": Object { + "mvn/version": "1.10.0", + }, + }, + }, + "1.9": Object { + "override-deps": Object { + "org.clojure/clojure": Object { + "mvn/version": "1.9.0", + }, + }, + }, + "datomic": Object { + "extra-deps": Object { + "com.datomic/datomic-free": Object { + "mvn/version": "0.9.5703", + }, + }, + }, + "dev": Object { + "extra-deps": Object { + "org.clojure/clojurescript": Object { + "mvn/version": "1.10.520", + }, + "org.clojure/tools.namespace": Object { + "mvn/version": "0.2.11", + }, + }, + "extra-paths": Array [ + "dev", + "target/classes", + ], + }, + "repl": Object { + "extra-deps": Object { + "cider/cider-nrepl": Object { + "mvn/version": "0.21.1", + }, + "nrepl/nrepl": Object { + "mvn/version": "0.6.0", + }, + "org.clojure/tools.namespace": Object { + "mvn/version": "0.2.11", + }, + }, + "main-opts": Array [ + "-m", + "nrepl.cmdline", + "--middleware", + "[cider.nrepl/cider-middleware]", + ], + }, + "test": Object { + "extra-deps": Object { + "io.github.lambdaisland/kaocha-cljs": Object { + "git/tag": "0.0-21", + }, + "lambdaisland/kaocha": Object { + "git/tag": "0.0-389", + "git/url": "https://github.com/lambdaisland/kaocha.git", + }, + "org.clojure/clojurescript": Object { + "mvn/version": "1.10.520", + }, + }, + "extra-paths": Array [ + "test", + ], + }, + "test-bitbucket": Object { + "extra-deps": Object { + "lambdaisland/kaocha": Object { + "git/tag": "0.0-389", + "git/url": "https://bitbucket.org/lambdaisland/kaocha.git", + }, + "org.bitbucket.lambdaisland/kaocha-cljs": Object { + "git/tag": "0.0-21", + }, + }, + "extra-paths": Array [ + "test", + ], + }, + "test-git": Object { + "extra-deps": Object { + "bar/bar": Object { + "git/url": "https://example.com/bar.git", + }, + "baz/baz": Object {}, + "foo/foo": Object { + "git/sha": "123", + "git/url": "git@example.com/foo.git", + }, + }, + "extra-paths": Array [ + "test", + ], + }, + "test-gitlab": Object { + "extra-deps": Object { + "com.gitlab.lambdaisland/kaocha-cljs": Object { + "git/tag": "0.0-21", + }, + "lambdaisland/kaocha": Object { + "git/tag": "0.0-389", + "git/url": "https://gitlab.com/lambdaisland/kaocha.git", + }, + }, + "extra-paths": Array [ + "test", + ], + }, + }, + "deps": Object { + "invalid/package!": Object { + "mvn/version": "1.2.3", + }, + "invalid/version": "nil", + "persistent-sorted-set": Object { + "mvn/version": "0.1.2", + }, + }, + "mvn/repos": Object { + "central": "nil", + "clojars": Object { + "url": "https://deps.com/foo/bar", + }, + "my-auth-repo": Object { + "url": "https://my.auth.com/repo", + }, + "my-private-repo": Object { + "url": "s3://my-bucket/maven/releases", + }, + }, +} +`; diff --git a/lib/modules/manager/deps-edn/extract.spec.ts b/lib/modules/manager/deps-edn/extract.spec.ts index 7e5a588cb60b79228030c51607ed180c716cb51b..39a1cd48d9f485cbfbe5f7707b0abb1c28eacdd2 100644 --- a/lib/modules/manager/deps-edn/extract.spec.ts +++ b/lib/modules/manager/deps-edn/extract.spec.ts @@ -2,57 +2,63 @@ import { Fixtures } from '../../../../test/fixtures'; import { extractPackageFile } from './extract'; describe('modules/manager/deps-edn/extract', () => { - it('extractPackageFile', () => { - const { deps } = extractPackageFile(Fixtures.get('deps.edn')); - expect(deps).toMatchSnapshot([ - { - depName: 'persistent-sorted-set:persistent-sorted-set', - currentValue: '0.1.2', - }, - { - depName: 'org.clojure:clojure', - currentValue: '1.9.0', - }, - { - depName: 'org.clojure:clojure', - currentValue: '1.10.0', - }, - { - depName: 'org.clojure:clojurescript', - currentValue: '1.10.520', - }, - { - depName: 'org.clojure:tools.namespace', - currentValue: '0.2.11', - }, - { - depName: 'org.clojure:clojurescript', - currentValue: '1.10.520', - }, - { - depName: 'lambdaisland:kaocha', - currentValue: '0.0-389', - }, - { - depName: 'lambdaisland:kaocha-cljs', - currentValue: '0.0-21', - }, - { - depName: 'cider:cider-nrepl', - currentValue: '0.21.1', - }, - { - depName: 'nrepl:nrepl', - currentValue: '0.6.0', - }, - { - depName: 'org.clojure:tools.namespace', - currentValue: '0.2.11', - }, - { - depName: 'com.datomic:datomic-free', - currentValue: '0.9.5703', - }, - ]); + describe('extractPackageFile', () => { + it('returns null for invalid file', () => { + expect(extractPackageFile('123')).toBeNull(); + }); + + it('extractPackageFile', () => { + const res = extractPackageFile(Fixtures.get('deps.edn')); + const deps = res?.deps; + expect(deps).toMatchSnapshot([ + { + depName: 'persistent-sorted-set', + currentValue: '0.1.2', + registryUrls: [ + 'https://deps.com/foo/bar', + 'https://my.auth.com/repo', + 's3://my-bucket/maven/releases', + ], + }, + { depName: 'org.clojure/clojure', currentValue: '1.9.0' }, + { depName: 'org.clojure/clojure', currentValue: '1.10.0' }, + { depName: 'org.clojure/clojurescript', currentValue: '1.10.520' }, + { depName: 'org.clojure/tools.namespace', currentValue: '0.2.11' }, + { depName: 'org.clojure/clojurescript', currentValue: '1.10.520' }, + { + depName: 'lambdaisland/kaocha', + packageName: 'lambdaisland/kaocha', + currentValue: '0.0-389', + }, + { + depName: 'io.github.lambdaisland/kaocha-cljs', + currentValue: '0.0-21', + }, + { + depName: 'lambdaisland/kaocha', + currentValue: '0.0-389', + depType: 'test-gitlab', + }, + { + depName: 'com.gitlab.lambdaisland/kaocha-cljs', + currentValue: '0.0-21', + }, + { + depName: 'lambdaisland/kaocha', + currentValue: '0.0-389', + depType: 'test-bitbucket', + }, + { + depName: 'org.bitbucket.lambdaisland/kaocha-cljs', + currentValue: '0.0-21', + }, + { depName: 'foo/foo', currentDigest: '123', datasource: 'git-refs' }, + { depName: 'bar/bar', sourceUrl: 'https://example.com/bar' }, + { depName: 'cider/cider-nrepl', currentValue: '0.21.1' }, + { depName: 'nrepl/nrepl', currentValue: '0.6.0' }, + { depName: 'org.clojure/tools.namespace', currentValue: '0.2.11' }, + { depName: 'com.datomic/datomic-free', currentValue: '0.9.5703' }, + ]); + }); }); }); diff --git a/lib/modules/manager/deps-edn/extract.ts b/lib/modules/manager/deps-edn/extract.ts index 104edcd32e7f1af19c836ecbd25608a6f8a038c8..dc4beab35385f80d7e4f0d7167d82b7a61e200be 100644 --- a/lib/modules/manager/deps-edn/extract.ts +++ b/lib/modules/manager/deps-edn/extract.ts @@ -1,29 +1,262 @@ +import is from '@sindresorhus/is'; import { regEx } from '../../../util/regex'; +import { BitBucketTagsDatasource } from '../../datasource/bitbucket-tags'; import { ClojureDatasource } from '../../datasource/clojure'; -import { expandDepName } from '../leiningen/extract'; +import { CLOJARS_REPO } from '../../datasource/clojure/common'; +import { GitRefsDatasource } from '../../datasource/git-refs'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { GitlabTagsDatasource } from '../../datasource/gitlab-tags'; +import { MAVEN_REPO } from '../../datasource/maven/common'; import type { PackageDependency, PackageFile } from '../types'; +import { parseDepsEdnFile } from './parser'; +import type { + ParsedEdnData, + ParsedEdnMetadata, + ParsedEdnRecord, +} from './types'; -export function extractPackageFile(content: string): PackageFile { +const dependencyRegex = regEx( + /^(?<groupId>[a-zA-Z][-_a-zA-Z0-9]*(?:\.[a-zA-Z0-9][-_a-zA-Z0-9]*)*)(?:\/(?<artifactId>[a-zA-Z][-_a-zA-Z0-9]*(?:\.[a-zA-Z0-9][-_a-zA-Z0-9]*)*))?$/ +); + +function getPackageName(depName: string): string | null { + const matchGroups = dependencyRegex.exec(depName)?.groups; + if (matchGroups) { + const groupId = matchGroups.groupId; + const artifactId = matchGroups.artifactId + ? matchGroups.artifactId + : groupId; + return `${groupId}:${artifactId}`; + } + + return null; +} + +const githubDependencyRegex = regEx( + /^(?:com|io)\.github\.(?<packageName>[^/]+\/[^/]+)$/ +); +const gitlabDependencyRegex = regEx( + /^(?:com|io)\.gitlab\.(?<packageName>[^/]+\/[^/]+)$/ +); +const bitbucketDependencyRegex = regEx( + /^(?:org|io)\.bitbucket\.(?<packageName>[^/]+\/[^/]+)$/ +); + +function resolveGitPackageFromEdnKey( + dep: PackageDependency, + key: string +): void { + if (dep.datasource) { + return; + } + + const githubDependencyGroups = githubDependencyRegex.exec(key)?.groups; + if (githubDependencyGroups?.packageName) { + dep.datasource = GithubTagsDatasource.id; + dep.packageName = githubDependencyGroups.packageName; + return; + } + + const gitlabDependencyGroups = gitlabDependencyRegex.exec(key)?.groups; + if (gitlabDependencyGroups?.packageName) { + dep.datasource = GitlabTagsDatasource.id; + dep.packageName = gitlabDependencyGroups.packageName; + return; + } + + const bitbucketDependencyGroups = bitbucketDependencyRegex.exec(key)?.groups; + if (bitbucketDependencyGroups?.packageName) { + dep.datasource = BitBucketTagsDatasource.id; + dep.packageName = bitbucketDependencyGroups.packageName; + return; + } +} + +const githubUrlRegex = regEx( + /^(?:https:\/\/|git@)github\.com[/:](?<packageName>[^/]+\/[^/]+?)(?:\.git)?$/ +); +const gitlabUrlRegex = regEx( + /^(?:https:\/\/|git@)gitlab\.com[/:](?<packageName>[^/]+\/[^/]+?)(?:\.git)?$/ +); +const bitbucketUrlRegex = regEx( + /^(?:https:\/\/|git@)bitbucket\.org[/:](?<packageName>[^/]+\/[^/]+?)(?:\.git)?$/ +); + +function resolveGitPackageFromEdnVal( + dep: PackageDependency, + val: ParsedEdnRecord +): void { + const gitUrl = val['git/url']; + if (!is.string(gitUrl)) { + return; + } + + const githubMatchGroups = githubUrlRegex.exec(gitUrl)?.groups; + if (githubMatchGroups) { + dep.datasource = GithubTagsDatasource.id; + dep.packageName = githubMatchGroups.packageName; + dep.sourceUrl = `https://github.com/${dep.packageName}`; + return; + } + + const gitlabMatchGroups = gitlabUrlRegex.exec(gitUrl)?.groups; + const bitbucketMatchGroups = bitbucketUrlRegex.exec(gitUrl)?.groups; + + if (gitlabMatchGroups) { + dep.datasource = GitlabTagsDatasource.id; + dep.packageName = gitlabMatchGroups.packageName; + dep.sourceUrl = `https://gitlab.com/${dep.packageName}`; + return; + } + + if (bitbucketMatchGroups) { + dep.datasource = GitlabTagsDatasource.id; + dep.packageName = bitbucketMatchGroups.packageName; + dep.sourceUrl = `https://bitbucket.org/${dep.packageName}`; + return; + } + + dep.datasource = GitRefsDatasource.id; + dep.packageName = gitUrl; + if (gitUrl.startsWith('https://')) { + dep.sourceUrl = gitUrl.replace(/\.git$/, ''); + } +} + +function extractDependency( + key: string, + val: ParsedEdnData, + metadata: ParsedEdnMetadata, + mavenRegistries: string[], + depType?: string +): PackageDependency | null { + if (!is.plainObject(val)) { + return null; + } + + const packageName = getPackageName(key); + if (!packageName) { + return null; + } + const depName = key; + + const dep: PackageDependency = { + depName, + packageName, + currentValue: null, + ...metadata.get(val), + }; + + if (depType) { + dep.depType = depType; + } + + resolveGitPackageFromEdnVal(dep, val); + resolveGitPackageFromEdnKey(dep, key); + + if (dep.datasource) { + const gitTag = val['git/tag']; + if (is.string(gitTag)) { + dep.currentValue = gitTag; + } + + const gitSha = val['git/sha'] ?? val['sha']; + if (is.string(gitSha)) { + dep.currentDigest = gitSha; + dep.currentDigestShort = gitSha.slice(0, 7); + } + + return dep; + } + + const mvnVersion = val['mvn/version']; + if (is.string(mvnVersion)) { + dep.datasource = ClojureDatasource.id; + dep.currentValue = mvnVersion; + dep.packageName = packageName.replace('/', ':'); + dep.registryUrls = [...mavenRegistries]; + return dep; + } + + return null; +} + +function extractSection( + section: ParsedEdnData, + metadata: ParsedEdnMetadata, + mavenRegistries: string[], + depType?: string +): PackageDependency[] { const deps: PackageDependency[] = []; + if (is.plainObject(section)) { + for (const [key, val] of Object.entries(section)) { + const dep = extractDependency( + key, + val, + metadata, + mavenRegistries, + depType + ); + if (dep) { + deps.push(dep); + } + } + } + return deps; +} + +export function extractPackageFile(content: string): PackageFile | null { + const parsed = parseDepsEdnFile(content); + if (!parsed) { + return null; + } + const { data, metadata } = parsed; + + const deps: PackageDependency[] = []; + + // See: https://clojure.org/reference/deps_and_cli#_modifying_the_default_repositories + const registryMap: Record<string, string> = { + clojars: CLOJARS_REPO, + central: MAVEN_REPO, + }; + const mavenRepos = data['mvn/repos']; + if (is.plainObject(mavenRepos)) { + for (const [repoName, repoSpec] of Object.entries(mavenRepos)) { + if (is.string(repoName)) { + if (is.plainObject(repoSpec) && is.string(repoSpec.url)) { + registryMap[repoName] = repoSpec.url; + } else if (is.string(repoSpec) && repoSpec === 'nil') { + delete registryMap[repoName]; + } + } + } + } + const mavenRegistries: string[] = [...Object.values(registryMap)]; + + deps.push(...extractSection(data['deps'], metadata, mavenRegistries)); - const regex = regEx( - /([^{\s,]*)[\s,]*{[\s,]*:mvn\/version[\s,]+"([^"]+)"[\s,]*}/ - ); - let rest = content; - let match = regex.exec(rest); - let offset = 0; - while (match) { - const [wholeSubstr, depName, currentValue] = match; - offset += match.index + wholeSubstr.length; - rest = content.slice(offset); - match = regex.exec(rest); - - deps.push({ - datasource: ClojureDatasource.id, - depName: expandDepName(depName), - currentValue, - registryUrls: [], - }); + const aliases = data['aliases']; + if (is.plainObject(aliases)) { + for (const [depType, aliasSection] of Object.entries(aliases)) { + if (is.plainObject(aliasSection)) { + deps.push( + ...extractSection( + aliasSection['extra-deps'], + metadata, + mavenRegistries, + depType + ) + ); + deps.push( + ...extractSection( + aliasSection['override-deps'], + metadata, + mavenRegistries, + depType + ) + ); + } + } } return { deps }; diff --git a/lib/modules/manager/deps-edn/parser.spec.ts b/lib/modules/manager/deps-edn/parser.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b33a634cb03435c17cac5525188ab67ba1a3ea29 --- /dev/null +++ b/lib/modules/manager/deps-edn/parser.spec.ts @@ -0,0 +1,59 @@ +import is from '@sindresorhus/is'; +import { Fixtures } from '../../../../test/fixtures'; +import { parseDepsEdnFile } from './parser'; + +describe('modules/manager/deps-edn/parser', () => { + describe('parseEdnFile', () => { + test.each` + input | output + ${''} | ${undefined} + ${':foo'} | ${undefined} + ${'foo'} | ${undefined} + ${'1'} | ${undefined} + ${'1.5'} | ${undefined} + ${'1e1'} | ${undefined} + ${'1e-1'} | ${undefined} + ${'[]'} | ${undefined} + ${'}'} | ${undefined} + ${'{}'} | ${{}} + ${'{'} | ${{}} + ${'{:foo :foo}'} | ${{ foo: 'foo' }} + ${'{:foo foo}'} | ${{ foo: 'foo' }} + ${'{:foo 1}'} | ${{ foo: '1' }} + ${'{:foo 1.5}'} | ${{ foo: '1.5' }} + ${'{:foo 1e1}'} | ${{ foo: '1e1' }} + ${'{:foo 1e-1}'} | ${{ foo: '1e-1' }} + ${'{:foo {}}'} | ${{ foo: {} }} + ${'{{} :foo}'} | ${{}} + ${'{{} {}}'} | ${{}} + ${'{:foo :bar}'} | ${{ foo: 'bar' }} + ${'{:foo 1 :bar 2}'} | ${{ foo: '1', bar: '2' }} + ${'{:foo {:bar 2} :baz}'} | ${{ foo: { bar: '2' } }} + ${'{:foo [:bar :baz]}'} | ${{ foo: ['bar', 'baz'] }} + ${'{:foo {:bar :baz}}'} | ${{ foo: { bar: 'baz' } }} + ${'{:foo [{:bar :baz}]}'} | ${{ foo: [{ bar: 'baz' }] }} + ${'{:foo {:bar :baz}}'} | ${{ foo: { bar: 'baz' } }} + `(`'$input' parses to $output`, ({ input, output }) => { + const res = parseDepsEdnFile(input); + expect(res?.data).toEqual(output); + }); + + it('extracts file', () => { + const content = Fixtures.get('deps.edn'); + const res = parseDepsEdnFile(content); + + expect(res?.data).toMatchSnapshot({ + deps: { 'persistent-sorted-set': { 'mvn/version': '0.1.2' } }, + }); + + const dep = + is.plainObject(res) && + is.plainObject(res.data['deps']) && + is.plainObject(res.data['deps']['persistent-sorted-set']) && + res.data['deps']['persistent-sorted-set']; + expect(dep && res?.metadata?.get(dep)).toEqual({ + replaceString: '{:mvn/version,"0.1.2"}', + }); + }); + }); +}); diff --git a/lib/modules/manager/deps-edn/parser.ts b/lib/modules/manager/deps-edn/parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..caffdb2f400151d706b69de33e0820ce8c6ab76f --- /dev/null +++ b/lib/modules/manager/deps-edn/parser.ts @@ -0,0 +1,192 @@ +import is from '@sindresorhus/is'; +import moo from 'moo'; +import { logger } from '../../../logger'; +import type { + EdnMetadata, + ParsedEdnArray, + ParsedEdnMetadata, + ParsedEdnRecord, + ParsedEdnResult, + ParserState, + TokenTypes, +} from './types'; + +const lexerStates = { + main: { + comma: { match: ',' }, + lineComment: { match: /;.*?$/ }, + leftParen: { match: '(' }, + rightParen: { match: ')' }, + leftSquare: { match: '[' }, + rightSquare: { match: ']' }, + leftFigure: { match: '{' }, + rightFigure: { match: '}' }, + longDoubleQuoted: { + match: '"""', + push: 'longDoubleQuoted', + }, + doubleQuoted: { + match: '"', + push: 'doubleQuoted', + }, + // https://clojure.org/reference/reader#_reader_forms + keyword: { + match: + /:(?:[a-zA-Z*+!_'?<>=.-][a-zA-Z0-9*+!_'?<>=.-]*)(?:\/(?:[a-zA-Z*+!_'?<>=.-][a-zA-Z0-9*+!_'?<>=.-]*))?/, + value: (x: string) => x.slice(1), + }, + symbol: { + match: + /(?:[a-zA-Z*+!_'?<>=.-][a-zA-Z0-9*+!_'?<>=.-]*)(?:\/(?:[a-zA-Z*+!_'?<>=.-][a-zA-Z0-9*+!_'?<>=.-]*))?/, + }, + double: { + match: + /(?:[0-9]+\.[0-9]*|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?|(?:[0-9]+[eE][+-]?[0-9]+)/, + }, + rational: { match: /[0-9]+\/[0-9]+/ }, + integer: { match: /(?:0x[0-9a-fA-F]+|[0-9]+r[0-9a-zA-Z]+|[0-9]+)/ }, + unknown: moo.fallback, + }, + longDoubleQuoted: { + stringFinish: { match: '"""', pop: 1 }, + stringContent: moo.fallback, + }, + doubleQuoted: { + stringFinish: { match: '"', pop: 1 }, + stringContent: moo.fallback, + }, +}; + +type TokenType = TokenTypes<typeof lexerStates>; + +const lexer = moo.states(lexerStates); + +export function parseDepsEdnFile(content: string): ParsedEdnResult | null { + lexer.reset(content); + const tokens = [...lexer]; + lexer.reset(); + + const stack: ParserState[] = []; + let state: ParserState = { type: 'root', data: null }; + + const metadata: ParsedEdnMetadata = new WeakMap< + ParsedEdnRecord | ParsedEdnArray, + EdnMetadata + >(); + + const popState = (): boolean => { + const savedState = stack.pop(); + if (!savedState) { + return false; + } + + if (savedState.type === 'root') { + savedState.data = state.data; + state = savedState; + return false; + } + + if (savedState.type === 'record') { + if (savedState.skipKey) { + savedState.currentKey = null; + savedState.skipKey = false; + } else if (savedState.currentKey) { + savedState.data[savedState.currentKey] = state.data; + savedState.currentKey = null; + } else { + savedState.skipKey = true; + } + } + + if (savedState.type === 'array') { + savedState.data.push(state.data); + } + + state = savedState; + return true; + }; + + for (const token of tokens) { + const tokenType = token.type as TokenType; + const stateType = state.type; + + // istanbul ignore else: token type comprehension + if ( + tokenType === 'lineComment' || + tokenType === 'unknown' || + tokenType === 'doubleQuoted' || + tokenType === 'longDoubleQuoted' || + tokenType === 'stringFinish' || + tokenType === 'comma' + ) { + continue; + } else if ( + tokenType === 'rightParen' || + tokenType === 'rightSquare' || + tokenType === 'rightFigure' + ) { + if (state.type === 'record' || state.type === 'array') { + const { startIndex } = state; + const endIndex = token.offset + token.value.length; + const replaceString = content.slice(startIndex, endIndex); + metadata.set(state.data, { replaceString }); + } + + if (!popState()) { + break; + } + } else if (tokenType === 'leftParen' || tokenType === 'leftSquare') { + stack.push(state); + state = { + type: 'array', + startIndex: token.offset, + data: [], + }; + } else if (tokenType === 'leftFigure') { + stack.push(state); + state = { + type: 'record', + startIndex: token.offset, + data: {}, + skipKey: false, + currentKey: null, + }; + } else if ( + tokenType === 'symbol' || + tokenType === 'keyword' || + tokenType === 'stringContent' || + tokenType === 'double' || + tokenType === 'rational' || + tokenType === 'integer' + ) { + if (stateType === 'record') { + if (state.skipKey) { + state.currentKey = null; + state.skipKey = false; + } else if (state.currentKey) { + state.data[state.currentKey] = token.value; + state.currentKey = null; + } else { + state.currentKey = token.value; + } + } else if (stateType === 'array') { + state.data.push(token.value); + } else if (stateType === 'root') { + state.data = token.value; + } + } else { + const unknownType: never = tokenType; + logger.debug({ unknownType }, `Unknown token type for "deps.edn"`); + } + } + + while (stack.length) { + popState(); + } + + if (is.plainObject(state.data)) { + return { data: state.data, metadata }; + } + + return null; +} diff --git a/lib/modules/manager/deps-edn/types.ts b/lib/modules/manager/deps-edn/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ea5ec0ea4417df09c04fd57802457c5c0410d48 --- /dev/null +++ b/lib/modules/manager/deps-edn/types.ts @@ -0,0 +1,46 @@ +export type UnionToIntersection<T> = ( + T extends any ? (x: T) => any : never +) extends (x: infer R) => any + ? R + : never; +export type TokenTypes<T> = keyof UnionToIntersection<T[keyof T]>; + +export type ParsedEdnPrimitive = string | null; +export type ParsedEdnArray = ParsedEdnData[]; +export type ParsedEdnRecord = { [k: string]: ParsedEdnData }; +export type ParsedEdnData = + | ParsedEdnPrimitive + | ParsedEdnRecord + | ParsedEdnArray; + +export type ParserState = + | { + type: 'root'; + data: ParsedEdnData; + } + | { + type: 'array'; + startIndex: number; + data: ParsedEdnArray; + } + | { + type: 'record'; + skipKey: boolean; + currentKey: string | null; + startIndex: number; + data: ParsedEdnRecord; + }; + +export interface EdnMetadata { + replaceString: string; +} + +export type ParsedEdnMetadata = WeakMap< + ParsedEdnRecord | ParsedEdnArray, + EdnMetadata +>; + +export interface ParsedEdnResult { + data: ParsedEdnRecord; + metadata: ParsedEdnMetadata; +}