diff --git a/lib/modules/manager/sbt/extract.spec.ts b/lib/modules/manager/sbt/extract.spec.ts index 3e5560943f8..ce37a1d2822 100644 --- a/lib/modules/manager/sbt/extract.spec.ts +++ b/lib/modules/manager/sbt/extract.spec.ts @@ -96,6 +96,66 @@ describe('modules/manager/sbt/extract', () => { }); }); + it('extracts deps when scala version is defined in an object', () => { + const content = codeBlock` + val versions = new { + scala = "2.12.10" + example = "0.0.8" + } + scalaVersion := versions.scala + version := "3.2.1" + libraryDependencies += "org.example" % "foo" % versions.example + `; + expect(extractPackageFile(content)).toEqual({ + deps: [ + { + datasource: 'maven', + depName: 'scala', + packageName: 'org.scala-lang:scala-library', + currentValue: '2.12.10', + registryUrls: [], + separateMinorPatch: true, + }, + { + datasource: 'sbt-package', + depName: 'org.example:foo', + packageName: 'org.example:foo', + currentValue: '0.0.8', + registryUrls: [], + sharedVariableName: 'versions.example', + variableName: 'versions.example', + }, + ], + packageFileVersion: '3.2.1', + managerData: { + scalaVersion: '2.12', + }, + }); + }); + + it('skips deps when dotted symbolds do not resolve to anything', () => { + const content = codeBlock` + scalaVersion := versions.scala + version := "3.2.1" + libraryDependencies += "org.example" % "foo" % versions.example + `; + expect(extractPackageFile(content)).toEqual({ + deps: [ + { + datasource: 'sbt-package', + depName: 'org.example:foo', + packageName: 'org.example:foo', + currentValue: undefined, + registryUrls: [], + }, + ], + packageFileVersion: '3.2.1', + managerData: { + scalaVersion: undefined, + }, + }); + }); + it('extracts packageFileVersion when scala version is defined in a variable', () => { const content = ` val fileVersion = "1.2.3" diff --git a/lib/modules/manager/sbt/extract.ts b/lib/modules/manager/sbt/extract.ts index 57256c16406..9c9ff3841b4 100644 --- a/lib/modules/manager/sbt/extract.ts +++ b/lib/modules/manager/sbt/extract.ts @@ -40,6 +40,11 @@ interface Ctx { depType?: string; useScalaVersion?: boolean; variableName?: string; + + // Use to contain temporary object name + // when parsing anonymous object structs. + // See objectAssignmentMatch. + currentObjectName?: string; } const scala = lang.createLang('scala'); @@ -56,7 +61,26 @@ const scalaVersionMatch = q .sym('scalaVersion') .op(':=') .alt( - q.str((ctx, { value: scalaVersion }) => ({ ...ctx, scalaVersion })), + // Dotted symbol. Important to have this first in the `alt` as otherwise + // the tree walker will eager parse a simple symbol and stop there. + q + // keep the first part of the path in context + .sym((ctx, { value: currentVarName }) => ({ + ...ctx, + currentVarName, + })) + .op('.') + .sym((ctx, { value: varName }) => { + // build the full variable name from what we saved earlier + const dottedVariableName = `${ctx.currentVarName}.${varName}`; + const currentValue = ctx.vars[dottedVariableName]; + if (currentValue) { + ctx.scalaVersion = currentValue; + } + delete ctx.currentVarName; + return ctx; + }), + // Simple symbol defined previously q.sym((ctx, { value: varName }) => { const scalaVersion = ctx.vars[varName]; if (scalaVersion) { @@ -64,6 +88,8 @@ const scalaVersionMatch = q } return ctx; }), + // Direct string + q.str((ctx, { value: scalaVersion }) => ({ ...ctx, scalaVersion })), ) .handler((ctx) => { if (ctx.scalaVersion) { @@ -127,6 +153,41 @@ const variableDefinitionMatch = q ) .join(variableValueMatch); +// Parses objects fields of the form `{name} = "{version}"`. +const objectFieldMatch = q + .sym((ctx, { value: fieldName }) => ({ + ...ctx, + currentVarName: `${ctx.currentObjectName}.${fieldName}`, + })) + .op('=') + .str((ctx, { value }) => { + ctx.vars[ctx.currentVarName!] = value; + delete ctx.currentVarName; + return ctx; + }); + +// Parses anonymous objects of the form +// val {objName} = new { +// {name} = "{version}" +// } +// and introduces a `objName.name` variable with value "{version}" +// in the context. +const objectAssignmentMatch = q + .sym('val') + .sym((ctx, { value: currentObjectName }) => ({ + ...ctx, + currentObjectName, + })) + .op('=') + .sym('new') + .tree({ + search: q.many(objectFieldMatch), + }) + .handler((ctx) => { + delete ctx.currentObjectName; + return ctx; + }); + const groupIdMatch = q.alt( q.sym((ctx, { value: varName }) => { const currentGroupId = ctx.vars[varName]; @@ -139,8 +200,8 @@ const groupIdMatch = q.alt( ); const artifactIdMatch = q.alt( - q.sym((ctx, { value: varName }) => { - const artifactId = ctx.vars[varName]; + q.sym((ctx, { value }) => { + const artifactId = ctx.vars[value]; if (artifactId) { ctx.artifactId = artifactId; } @@ -150,6 +211,23 @@ const artifactIdMatch = q.alt( ); const versionMatch = q.alt( + // Dotted symbol. Important to have this first in the `alt` as otherwise + // the tree walker will eager parse a simple symbol and stop there. + q + .sym((ctx, { value: currentVarName }) => ({ ...ctx, currentVarName })) + .op('.') + .sym((ctx, { value: varName }) => { + // build the full variable name from what we saved earlier + const dottedVariableName = `${ctx.currentVarName}.${varName}`; + const currentValue = ctx.vars[dottedVariableName]; + if (currentValue) { + ctx.currentValue = currentValue; + ctx.variableName = dottedVariableName; + } + delete ctx.currentVarName; + return ctx; + }), + // Simple symbol defined earlier q.sym((ctx, { value: varName }) => { const currentValue = ctx.vars[varName]; if (currentValue) { @@ -158,6 +236,7 @@ const versionMatch = q.alt( } return ctx; }), + // Direct string q.str((ctx, { value: currentValue }) => ({ ...ctx, currentValue })), ); @@ -292,6 +371,7 @@ const query = q.tree({ sbtPluginMatch, addResolverMatch, variableDefinitionMatch, + objectAssignmentMatch, ), postHandler: registryUrlHandler, });