Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions lib/modules/manager/sbt/extract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
86 changes: 83 additions & 3 deletions lib/modules/manager/sbt/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -56,14 +61,35 @@ const scalaVersionMatch = q
.sym<Ctx>('scalaVersion')
.op(':=')
.alt(
q.str<Ctx>((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>((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>((ctx, { value: varName }) => {
const scalaVersion = ctx.vars[varName];
if (scalaVersion) {
ctx.scalaVersion = scalaVersion;
}
return ctx;
}),
// Direct string
q.str<Ctx>((ctx, { value: scalaVersion }) => ({ ...ctx, scalaVersion })),
)
.handler((ctx) => {
if (ctx.scalaVersion) {
Expand Down Expand Up @@ -127,6 +153,41 @@ const variableDefinitionMatch = q
)
.join(variableValueMatch);

// Parses objects fields of the form `{name} = "{version}"`.
const objectFieldMatch = q
.sym<Ctx>((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<Ctx>('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<Ctx>(
q.sym<Ctx>((ctx, { value: varName }) => {
const currentGroupId = ctx.vars[varName];
Expand All @@ -139,8 +200,8 @@ const groupIdMatch = q.alt<Ctx>(
);

const artifactIdMatch = q.alt<Ctx>(
q.sym<Ctx>((ctx, { value: varName }) => {
const artifactId = ctx.vars[varName];
q.sym<Ctx>((ctx, { value }) => {
const artifactId = ctx.vars[value];
if (artifactId) {
ctx.artifactId = artifactId;
}
Expand All @@ -150,6 +211,23 @@ const artifactIdMatch = q.alt<Ctx>(
);

const versionMatch = q.alt<Ctx>(
// 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>((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>((ctx, { value: varName }) => {
const currentValue = ctx.vars[varName];
if (currentValue) {
Expand All @@ -158,6 +236,7 @@ const versionMatch = q.alt<Ctx>(
}
return ctx;
}),
// Direct string
q.str<Ctx>((ctx, { value: currentValue }) => ({ ...ctx, currentValue })),
);

Expand Down Expand Up @@ -292,6 +371,7 @@ const query = q.tree<Ctx>({
sbtPluginMatch,
addResolverMatch,
variableDefinitionMatch,
objectAssignmentMatch,
),
postHandler: registryUrlHandler,
});
Expand Down
Loading