diff --git a/cli/internal/utils/project/config.go b/cli/internal/utils/project/config.go index ce79b5219..5c64aeac6 100644 --- a/cli/internal/utils/project/config.go +++ b/cli/internal/utils/project/config.go @@ -15,10 +15,32 @@ type Config struct { } type JavaProject struct { - SourceRoot string `yaml:"sourceRoot"` - JavaToolchain string `yaml:"javaToolchain,omitempty"` - Modules []Module `yaml:"modules"` - Dependencies []string `yaml:"dependencies,omitempty"` + SourceRoot string `yaml:"sourceRoot"` + JavaToolchain string `yaml:"javaToolchain,omitempty"` + Modules []Module `yaml:"modules"` + Dependencies []ResolvedDependency `yaml:"dependencies,omitempty"` +} + +type ResolvedDependency struct { + Path string `yaml:"path"` + Purl string `yaml:"purl,omitempty"` +} + +// UnmarshalYAML accepts a legacy bare path string (as a path-only dependency) or the tagged mapping. +func (d *ResolvedDependency) UnmarshalYAML(unmarshal func(any) error) error { + var bareString string + if err := unmarshal(&bareString); err == nil { + d.Path = bareString + return nil + } + + type plain ResolvedDependency + var p plain + if err := unmarshal(&p); err != nil { + return err + } + *d = ResolvedDependency(p) + return nil } type GoProject struct { @@ -32,10 +54,10 @@ type Module struct { } type legacyConfig struct { - SourceRoot string `yaml:"sourceRoot"` - JavaToolchain string `yaml:"javaToolchain,omitempty"` - Modules []Module `yaml:"modules"` - Dependencies []string `yaml:"dependencies,omitempty"` + SourceRoot string `yaml:"sourceRoot"` + JavaToolchain string `yaml:"javaToolchain,omitempty"` + Modules []Module `yaml:"modules"` + Dependencies []ResolvedDependency `yaml:"dependencies,omitempty"` } func LoadConfig(projectModelPath string) (*Config, error) { @@ -79,7 +101,9 @@ func (c *Config) AllModules() []Module { func (c *Config) AllDependencies() []string { var deps []string for _, jp := range c.JavaProjects { - deps = append(deps, jp.Dependencies...) + for _, d := range jp.Dependencies { + deps = append(deps, d.Path) + } } return deps } diff --git a/cli/internal/utils/project/config_test.go b/cli/internal/utils/project/config_test.go index 1c5c84247..c64a9d737 100644 --- a/cli/internal/utils/project/config_test.go +++ b/cli/internal/utils/project/config_test.go @@ -4,6 +4,8 @@ import ( "os" "path/filepath" "testing" + + "gopkg.in/yaml.v2" ) func writeProjectYaml(t *testing.T, content string) string { @@ -23,7 +25,9 @@ javaProjects: - moduleSourceRoot: java_0/sources packages: [com.example] moduleClasses: [java_0/classes/main] - dependencies: [libs/a.jar] + dependencies: + - path: libs/a.jar + purl: pkg:maven/org.opensearch.client/opensearch-rest-client@2.18.0 goProjects: - projectDir: go_0 `) @@ -44,11 +48,35 @@ goProjects: if got := config.AllDependencies(); len(got) != 1 || got[0] != "libs/a.jar" { t.Errorf("AllDependencies = %+v", got) } + dep := config.JavaProjects[0].Dependencies[0] + if dep.Purl != "pkg:maven/org.opensearch.client/opensearch-rest-client@2.18.0" { + t.Fatalf("expected purl-tagged dependency, got %+v", dep) + } if config.GoProjects[0].ProjectDir != "go_0" { t.Errorf("go projectDir = %q", config.GoProjects[0].ProjectDir) } } +func TestUnmarshalDependencyMappingWithoutPurl(t *testing.T) { + yamlData := []byte(`javaProjects: +- dependencies: + - path: /d/lib.jar +`) + + var config Config + if err := yaml.Unmarshal(yamlData, &config); err != nil { + t.Fatalf("yaml.Unmarshal: %v", err) + } + + if got := config.AllDependencies(); len(got) != 1 || got[0] != "/d/lib.jar" { + t.Fatalf("AllDependencies = %+v", got) + } + dep := config.JavaProjects[0].Dependencies[0] + if dep.Purl != "" { + t.Errorf("Purl = %q, want empty", dep.Purl) + } +} + func TestLoadConfigLegacyFallback(t *testing.T) { dir := writeProjectYaml(t, `sourceRoot: src javaToolchain: /jdk @@ -56,7 +84,9 @@ modules: - moduleSourceRoot: src packages: [com.legacy] moduleClasses: [dist/app.jar] -dependencies: [lib/commons-io.jar] +dependencies: + - path: lib/commons-io.jar + purl: pkg:maven/commons-io/commons-io@2.16.1 `) config, err := LoadConfig(dir) @@ -81,6 +111,42 @@ dependencies: [lib/commons-io.jar] } } +func TestLoadConfigLegacyBareStringDependencyLoadsAsPathOnly(t *testing.T) { + dir := writeProjectYaml(t, `javaProjects: + - sourceRoot: src + dependencies: + - /path/to/a.jar + - /path/to/b.jar +`) + + config, err := LoadConfig(dir) + if err != nil { + t.Fatalf("LoadConfig: %v", err) + } + got := config.AllDependencies() + if len(got) != 2 || got[0] != "/path/to/a.jar" || got[1] != "/path/to/b.jar" { + t.Fatalf("AllDependencies = %+v", got) + } + for _, dep := range config.JavaProjects[0].Dependencies { + if dep.Purl != "" { + t.Errorf("Purl = %q, want empty", dep.Purl) + } + } +} + +func TestAllDependenciesReturnsPaths(t *testing.T) { + c := &Config{JavaProjects: []JavaProject{{ + Dependencies: []ResolvedDependency{ + {Path: "/d/os-2.18.0.jar", Purl: "pkg:maven/org.opensearch.client/opensearch-rest-client@2.18.0"}, + {Path: "/d/os-3.5.0.jar", Purl: "pkg:maven/org.opensearch.client/opensearch-rest-client@3.5.0"}, + }, + }}} + got := c.AllDependencies() + if len(got) != 2 || got[0] != "/d/os-2.18.0.jar" || got[1] != "/d/os-3.5.0.jar" { + t.Fatalf("unexpected: %v", got) + } +} + func TestGetSourceRootRelativeResolved(t *testing.T) { dir := writeProjectYaml(t, `javaProjects: - sourceRoot: java_0/sources diff --git a/docs/classes-and-jars-analysis.md b/docs/classes-and-jars-analysis.md index cba202fe0..8e0507475 100644 --- a/docs/classes-and-jars-analysis.md +++ b/docs/classes-and-jars-analysis.md @@ -26,8 +26,10 @@ javaProjects: moduleClasses: - /path/to/module2.jar dependencies: # optional - - /path/to/dependency1.jar - - /path/to/dependency2.jar + - path: /path/to/opensearch-rest-client-2.18.0.jar + purl: pkg:maven/org.opensearch.client/opensearch-rest-client@2.18.0 + - path: /path/to/dependency-without-coordinates.jar # no purl + - /path/to/legacy-bare-string.jar # legacy: loaded path-only goProjects: # optional - projectDir: /path/to/go/module ``` @@ -50,7 +52,11 @@ List of Java projects. Each entry describes one Java project (a multi-module bui - **moduleSourceRoot** (required): Path to the module's source code directory - **packages** (required): List of Java packages contained in this module - **moduleClasses** (required): List of paths to compiled classes or JAR files for this module -- **dependencies** (optional): Array of paths to JAR files the project depends on (typically third-party libraries). +- **dependencies** (optional): The project's dependency JARs (typically third-party libraries). Each entry is an object: + - **path** (required): Path to the JAR file. + - **purl** (optional): The dependency's package-URL (`pkg://@`, e.g. `pkg:maven/org.opensearch.client/opensearch-rest-client@2.18.0` for JVM, `pkg:golang/...` for Go). Omitted for coordinate-less dependencies (e.g. a JAR supplied via `--dependency`). + + For backward compatibility a dependency may also be written as a bare path string (`- /path/to/lib.jar`); it is loaded as a path-only dependency (no purl). New models are generated in the object form above. ### goProjects (optional) List of Go projects. Each entry has a single field, **projectDir**, pointing at the Go module directory.