Maven & Gradle
Gradle resolves coordinates against the same Maven Central registry client Maven uses (falling back to the Gradle Plugin Portal for a group ID not found there), so most of Maven’s behavior — version comparison, range matching, freshness — applies identically to both ecosystems. This chapter documents them together; ecosystem-specific notes are called out where they diverge.
Basics
Maven manifests are pom.xml files. deps-lsp reads <dependency> entries under
<dependencies> and <dependencyManagement>, plus <plugin> entries under <build><plugins>:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>
Hovering over commons-lang3 or 3.14.0 queries Maven Central
(repo1.maven.org/maven2) for the artifact’s maven-metadata.xml, showing the latest
release and recent version history. A groupId under androidx.*, com.google.firebase.*,
com.google.android.*, com.google.gms.*, or com.android.* resolves against Google
Maven (dl.google.com/dl/android/maven2) instead — Google does not mirror these artifacts
to Maven Central. Any group ID not found on Maven Central also falls back to the Gradle
Plugin Portal (plugins.gradle.org/m2), which is how a coordinate that is really a Gradle
plugin (declared as a plain dependency, not a plugins {} block) still resolves. Completion
for <groupId>/<artifactId>/<version> uses Maven Central’s Solr search API
(search.maven.org/solrsearch). Maven has no lock file — the version written in pom.xml (or
resolved through a ${property} reference, see below) is always the “in-use” version.
Gradle manifests are build.gradle (Groovy DSL), build.gradle.kts (Kotlin DSL),
settings.gradle/settings.gradle.kts (for pluginManagement {} dependencies), and
gradle/libs.versions.toml (the Gradle version catalog format). A dependency declared in
any Gradle configuration — implementation, api, testImplementation,
androidTestImplementation, compileOnly, classpath, the legacy compile/testCompile/
provided, and their variant-prefixed forms — is recognized:
dependencies {
implementation("com.google.guava:guava:33.0.0-jre")
testImplementation("junit:junit:4.13.2")
}
# gradle/libs.versions.toml
[versions]
guava = "33.0.0-jre"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
deps-gradle’s own parser dispatches by file name/extension into a dedicated Groovy, Kotlin,
.properties, settings.gradle(.kts), or version-catalog sub-parser — but all of them resolve
coordinates through the same Maven Central registry client Maven uses (see the top of this
page), so hover/completion/diagnostics behavior described for Maven below applies to Gradle too
unless a section says otherwise. Gradle has no lock file either.
Non-Registry Dependency Sources (Maven)
A Maven <dependency> with <scope>system</scope> and a <systemPath> — an explicit
locally-provided JAR, never resolved from Maven Central — is classified as a non-registry
dependency instead of defaulting to Maven Central. A dependency resolved this way is never
sent to Central, drops its public-registry hover link, and is excluded from OSV
vulnerability scanning against the public artifact coordinates (resolves #1202). A
system-scope dependency with a missing or empty <systemPath> still falls back to
Central-resolvable, since scope alone isn’t a locally-provided binding without a path.
A <repositories>/<repository><url>file://...</url> declaration is a second, independent
non-registry source: since Maven’s <repository> element has no per-package binding (every
declared repository is tried, in order, for any dependency), each dependency in that
pom.xml is individually probed against the standard Maven repository layout
(<group-path>/<artifact>/<version>/<artifact>-<version>.jar) under every declared file://
repository directory, and only classified as non-registry when its jar is actually found
there (resolves #1503). A dependency with no matching jar under any file:// repository — an
implicit Maven Central dependency declared in the same file, for example — keeps its normal
Central-resolvable classification; a version-range requirement (e.g. [1.0,2.0)) and a
<plugin> entry (which resolves through the separate <pluginRepositories> config) are
never probed this way. ${project.basedir} (the pom.xml’s own containing directory) resolves
correctly in the repository URL, but a file:// URL that carries a host — a potential
Windows UNC path — is rejected outright rather than probed, since resolving it could make
Windows silently attempt SMB authentication against that host.
Known limitation: a file:// repository that happens to mirror or cache Maven Central
(e.g. a local ~/.m2/repository cache or an offline CI mirror declared as a <repository>)
causes every dependency whose jar is actually cached there to lose OSV/outdated coverage,
even though it is really a Central-sourced dependency — the probe has no way to distinguish
“genuinely local-only” from “a local mirror of a public registry.” The probing pass also
spends a bounded total budget of filesystem checks across the whole document; once
exhausted, every remaining dependency keeps its current (usually Registry) classification,
so a genuinely local dependency declared very late in an unusually large pom.xml may be
missed.
Gradle: Gradle has no per-dependency local-source syntax analogous to Maven’s
systemPath (project(":core")/files()/fileTree() dependencies are deliberately not
surfaced as version-checkable dependencies at all, so no Gradle dependency ever carries a
local source through this pipeline that way). Instead, a repositories { <repo> { content { includeGroup(...)/includeGroupByRegex(...)/includeModule(...) } } } restriction (Gradle 6+,
Groovy and Kotlin DSL, including maven("url") { }/url.set(uri("...")) call-site
spellings) is read as a per-dependency non-registry classification signal: a dependency
whose group matches a content {} restriction scoped to a repository with an explicit URL
is classified as a custom-registry source the same way Maven’s systemPath is (resolves
#1212). An explicit repository URL is required — the common shorthand repos
google()/mavenCentral()/gradlePluginPortal()/mavenLocal() (which frequently carry
their own content {} filter, e.g. Android’s canonical google { content { includeGroupByRegex("androidx.*") } }) are never reclassified this way, since they are
themselves registry-shaped. A content {} restriction declared inside a buildscript {}
block (plugin resolution) never affects the project’s own dependencies {} classification.
Known limitation: exclusiveContent {} and includeGroupAndSubgroups(...) are not yet
parsed — only includeGroup/includeGroupByRegex/includeModule inside an ordinary
content {} block.
Version Completion (Maven)
Version completion is offered for a self-closing <version/> tag, not just <version>X</version>
or an empty <version></version>: accepting a completion item there replaces the whole
<version/> span with <version>X</version> via an explicit text edit, rather than inserting text
at the cursor (which would otherwise land just after /> and corrupt the surrounding XML).
Version Comparison
Versions are now ranked with correct Maven semantics:
- Numeric segments outrank non-numeric qualifiers:
33>r09(previously the reverse) - Prerelease qualifiers sort below their base release:
1.0-RC1<1.0(previously the reverse) - Qualifier precedence:
alpha<beta<milestone<rc/cr<snapshot<release<sp(case-insensitive) - Numeric suffixes within qualifiers are compared numerically:
M10>M2(previouslyM2>M10)
These fixes ensure hover’s “Recent versions” list and completion sort order match Maven’s actual version ordering.
Version Range Matching
version_satisfies_requirement recognizes bracket-interval range syntax instead of only exact string equality, so a dependency pinned to a range no longer always renders as “outdated”:
- Maven (
pom.xml): interval notation —[1.0,2.0),[1.0](exact pin),[1.5,),(,2.0]— and top-level comma unions, e.g.(,1.0),(1.2,). Bounds are compared with Maven’s qualifier-aware ordering, so[1.0-beta,2.0-rc)orders correctly. A bare, non-bracketed requirement (1.0) is still Maven’s “soft” recommended version and compared for plain equality, not as a range. - Gradle (
build.gradle,build.gradle.kts,gradle/libs.versions.toml): the same bracket-interval syntax as Maven (no comma unions — Gradle’s grammar doesn’t have them), plus Gradle-specific forms: dynamic versions (1.0+,2.10.+),latest.release/latest.integrationselectors, and Gradle’s reversed-bracket exclusive notation (]1.2,1.5]for an exclusive lower bound,[1.1,2.0[for an exclusive upper bound). - Malformed input fails closed: an unparseable range (unbalanced/stray brackets, an extra comma-separated component, a mismatched no-comma pin like
[1.0), or any unparseable member of a Maven union) is rejected as a whole —version_satisfies_requirementreturnsfalserather than matching on a corrupted or partial parse.
Unresolved Requirements
A requirement that couldn’t be resolved to a concrete version (Maven’s ${property} missing from <properties>, Gradle’s $var/${var} variable reference, or a Gradle version-catalog version.ref alias missing from [versions]) is treated as RequirementStatus::Unresolved, distinct from UpToDate/Outdated:
- Diagnostics: no “Newer version available” hint is shown — same as before, since the server can’t verify either way.
- Inlay hints: no badge is shown at all, neither “up to date” nor “needs update” — showing “up to date” for a requirement that was never actually checked against the latest version would be misleading.
- CodeLens “Update N outdated dependencies” (see Version Diagnostics): an unresolved requirement is also never counted or edited — it already fails the literal-span guard (the tracked span covers a placeholder/variable, not a version literal), so the two mechanisms agree independently rather than one depending on the other.
Release-Freshness Coverage
Hover’s “Recent versions” age suffix and completion’s age label_details (gated by
freshness.enabled, default true) depend on Maven Central’s repo1.maven.org HTML
directory listing, which is not available for every artifact source Maven/Gradle resolve
through:
- Maven Central (
repo1.maven.org) — the directory listing is fetched and parsed; ages render normally. - Google Maven (
dl.google.com,androidx.*/com.google.firebase.*/com.google.android.*/com.google.gms.*/com.android.*group IDs) — the listing 404s for every artifact, so no extra request is even attempted;published_at()is alwaysNoneand the version list itself is unaffected. - Gradle Plugin Portal (
plugins.gradle.org, the fallback for a group ID not found on Maven Central) — the listing has no date column; same result as above.
This is intentional graceful degradation (US-003), the same shape as Go’s documented
partial freshness coverage (/@v/list carries no per-version dates either) — not a bug.
License Hover (Gradle)
Gradle’s license hover follows the Maven Central POM’s <parent> coordinate chain when a
dependency’s own POM declares no <licenses> block — see License
Hover for the full cross-ecosystem picture and
the license policy diagnostic that consumes the same normalized data.