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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,29 @@ convert the value with knowledge of the source format.

When a dependency comes from a non-default registry, the PURL includes a `repository_url` qualifier (e.g., `pkg:npm/foo@1.0.0?repository_url=https://npm.mycompany.com/`). Default registries like registry.npmjs.org, pypi.org, and rubygems.org are not included in the PURL.

### Declaration

```go
type Declaration struct {
Name string // Package name
Version string // Version requirement as written in the manifest
Scope Scope // runtime, development, test, build, optional
PURL string // Versionless Package URL
Location string // Opaque parser-defined identity within the manifest
}
```

Declarations preserve source-level references without applying inheritance,
merging, interpolation, or other effective-model resolution. Consumers can use
`Location` to match the same logical entry across edits, but should not parse
its ecosystem-specific value. A declaration PURL omits the version because the
raw requirement may be a range or property expression.

Parsers that do not preserve source locations leave `Declarations` empty.
The `pom.xml` parser populates parents, dependencies, dependency management,
plugins, plugin dependencies, plugin management, build extensions, and their
profile-scoped forms.

### ParseResult

```go
Expand All @@ -234,6 +257,7 @@ type ParseResult struct {
Licenses []string // raw declared license values
LicenseFile string // manifest-relative path to a declared license file
Dependencies []Dependency
Declarations []Declaration
}
```

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v1.6.0
github.com/bazelbuild/buildtools v0.0.0-20260716142318-04cf7de1434f
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/git-pkgs/pom v0.1.6
github.com/git-pkgs/pom v0.1.7
github.com/git-pkgs/purl v0.1.16
gopkg.in/yaml.v3 v3.0.1
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ github.com/bazelbuild/buildtools v0.0.0-20260716142318-04cf7de1434f h1:2mT6QcXmM
github.com/bazelbuild/buildtools v0.0.0-20260716142318-04cf7de1434f/go.mod h1:PLNUetjLa77TCCziPsz0EI8a6CUxgC+1jgmWv0H25tg=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/git-pkgs/pom v0.1.6 h1:OecrZgRChYQybf35YVF5yKfPIh6zsJ5gGgE3FKghJoc=
github.com/git-pkgs/pom v0.1.6/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk=
github.com/git-pkgs/pom v0.1.7 h1:4yKdtw6eyShtjul6bcZdyz7yLQ+jdrYeYkKbskDGi4c=
github.com/git-pkgs/pom v0.1.7/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk=
github.com/git-pkgs/purl v0.1.16 h1:VAX6tv0hhdTENbkrGMoPZbOAl1Y8U1/ZnzoCsYuNBYM=
github.com/git-pkgs/purl v0.1.16/go.mod h1:7u7ora8tQdrkS7Auclr5v8dCJdjN4ej6AbrvYZi2b7k=
github.com/git-pkgs/vers v0.3.1 h1:jy/ht2wIRJI5zQrccm6GTeYr+hGFwe2z8LV1HOr4Wco=
Expand Down
13 changes: 13 additions & 0 deletions internal/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ type Dependency struct {
RegistryURL string
}

// Declaration is a dependency-like reference at a stable logical location
// in a manifest. Version is the requirement as written in that location,
// before effective-model resolution or inheritance. Location is
// ecosystem-specific and should be treated as an opaque identity.
type Declaration struct {
Name string
Version string
Scope Scope
PURL string
Location string
}

// Result is the output of a single parser.
type Result struct {
// Name is the package's own name as declared in the manifest, when
Expand All @@ -49,6 +61,7 @@ type Result struct {
// LicenseFile is a manifest-relative path to a declared license file.
LicenseFile string
Dependencies []Dependency
Declarations []Declaration
}

// Parser is the interface implemented by all manifest parsers.
Expand Down
145 changes: 145 additions & 0 deletions internal/maven/declarations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package maven

import (
"net/url"
"strings"

"github.com/git-pkgs/manifests/internal/core"
"github.com/git-pkgs/pom"
)

const defaultMavenPluginGroup = "org.apache.maven.plugins"

func parsePOMDeclarations(project *pom.POM) []core.Declaration {
var declarations []core.Declaration
if project.Parent != nil {
parent := project.Parent
appendMavenDeclaration(
&declarations,
"parent",
parent.GroupID,
parent.ArtifactID,
parent.Version,
core.Build,
"",
)
}
collectPOMDependencies(&declarations, "dependencies", project.Dependencies)
collectPOMDependencies(&declarations, "dependencyManagement/dependencies", project.DependencyManagement.Dependencies)
collectMavenPlugins(&declarations, "build/plugins", project.Build.Plugins)
collectMavenPlugins(&declarations, "build/pluginManagement/plugins", project.Build.PluginManagement.Plugins)
collectMavenExtensions(&declarations, "build/extensions", project.Build.Extensions)
Comment on lines +27 to +31

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relevant fields in pom v0.1.7 are value fields: DepMgmt, Build, and PluginManagement. Omitted sections therefore produce zero values rather than nil pointers, so these dereferences cannot panic. No change is needed here.


for _, profile := range project.Profiles {
profileID := strings.TrimSpace(profile.ID)
if profileID == "" {
profileID = "default"
}
prefix := "profiles/" + url.PathEscape(profileID)
collectPOMDependencies(&declarations, prefix+"/dependencies", profile.Dependencies)
collectPOMDependencies(&declarations, prefix+"/dependencyManagement/dependencies", profile.DependencyManagement.Dependencies)
collectMavenPlugins(&declarations, prefix+"/build/plugins", profile.Build.Plugins)
collectMavenPlugins(&declarations, prefix+"/build/pluginManagement/plugins", profile.Build.PluginManagement.Plugins)
collectMavenExtensions(&declarations, prefix+"/build/extensions", profile.Build.Extensions)
}

return declarations
}

func collectPOMDependencies(declarations *[]core.Declaration, location string, dependencies []pom.Dep) {
for _, dependency := range dependencies {
optional := strings.EqualFold(strings.TrimSpace(dependency.Optional), "true")
appendMavenDeclaration(
declarations,
location,
dependency.GroupID,
dependency.ArtifactID,
dependency.Version,
mapScope(dependency.Scope, optional),
"",
dependency.Type,
dependency.Classifier,
)
}
}

func collectMavenPlugins(declarations *[]core.Declaration, location string, plugins []pom.Plugin) {
for _, plugin := range plugins {
pluginName := appendMavenDeclaration(
declarations,
location,
plugin.GroupID,
plugin.ArtifactID,
plugin.Version,
core.Build,
defaultMavenPluginGroup,
)
if pluginName == "" {
continue
}
dependencyLocation := location + "/" + url.PathEscape(pluginName) + "/dependencies"
for _, dependency := range plugin.Dependencies {
appendMavenDeclaration(
declarations,
dependencyLocation,
dependency.GroupID,
dependency.ArtifactID,
dependency.Version,
core.Build,
"",
)
}
Comment on lines +80 to +91

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build scope is intentional here. Declaration.Scope describes when the referenced package is required by the project. Plugin dependencies are loaded on the plugin classpath during the build; mapping an omitted nested Maven scope to Runtime would misclassify them for manifests consumers.

}
}

func collectMavenExtensions(declarations *[]core.Declaration, location string, extensions []pom.Extension) {
for _, extension := range extensions {
appendMavenDeclaration(
declarations,
location,
extension.GroupID,
extension.ArtifactID,
extension.Version,
core.Build,
"",
)
}
}

func appendMavenDeclaration(
declarations *[]core.Declaration,
location string,
groupID string,
artifactID string,
version string,
scope core.Scope,
defaultGroup string,
qualifiers ...string,
) string {
groupID = strings.TrimSpace(groupID)
artifactID = strings.TrimSpace(artifactID)
if artifactID == "" {
return ""
}
if groupID == "" {
groupID = defaultGroup
}
if groupID == "" {
return ""
}
Comment thread
Copilot marked this conversation as resolved.

name := groupID + ":" + artifactID
key := name
for _, qualifier := range qualifiers {
if qualifier = strings.TrimSpace(qualifier); qualifier != "" {
key += ":" + qualifier
}
}
*declarations = append(*declarations, core.Declaration{
Name: name,
Version: strings.TrimSpace(version),
Scope: scope,
Location: location + "/" + url.PathEscape(key),
})
return name
}
Loading