Copilot commented on code in PR #255:
URL: https://github.com/apache/skywalking-eyes/pull/255#discussion_r2636590290
##########
pkg/deps/ruby.go:
##########
@@ -253,6 +416,193 @@ func runtimeDepsFromGemspecs(dir string) ([]string,
error) {
return res, nil
}
+func parseGemspecDependencies(path string) ([]string, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ var deps []string
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimLeft := strings.TrimLeft(line, " \t")
+ if strings.HasPrefix(trimLeft, "#") {
+ continue
+ }
+ if m := gemspecRuntimeRe.FindStringSubmatch(line); len(m) == 2 {
+ deps = append(deps, m[1])
+ }
+ }
+ return deps, scanner.Err()
+}
+
+func findInstalledGemspec(name, version string) (string, error) {
+ gemPaths := getGemPaths()
+ for _, dir := range gemPaths {
+ specsDir := filepath.Join(dir, "specifications")
+ if version != "" && rubyVersionRe.MatchString(version) {
+ path := filepath.Join(specsDir,
name+"-"+version+".gemspec")
+ if _, err := os.Stat(path); err == nil {
+ return path, nil
+ }
+ } else {
+ entries, err := os.ReadDir(specsDir)
+ if err != nil {
+ continue
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasPrefix(e.Name(),
name+"-") || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ stem := strings.TrimSuffix(e.Name(), ".gemspec")
+ // Ensure that the character immediately after
the "name-" prefix
+ // is a digit, so we only consider filenames
where the suffix is
+ // a version component (e.g.,
"foo-1.0.0.gemspec") and avoid
+ // similar names like "foo-bar-1.0.0.gemspec"
when searching for "foo".
+ if len(stem) <= len(name)+1 {
+ continue
+ }
+ versionStart := stem[len(name)+1]
+ if versionStart < '0' || versionStart > '9' {
+ continue
+ }
+ ver := strings.TrimPrefix(stem, name+"-")
+ if ver == stem {
+ continue
+ }
+ path := filepath.Join(specsDir, e.Name())
+ if specName, _, err := parseGemspecInfo(path);
err == nil && specName == name {
+ return path, nil
+ }
+ }
+ }
+ }
+ return "", os.ErrNotExist
+}
+
+func fetchLocalLicense(dir, targetName string) (string, error) {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return "", err
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ path := filepath.Join(dir, e.Name())
+ specName, license, err := parseGemspecInfo(path)
+ if err == nil && specName == targetName && license != "" {
+ return license, nil
+ }
+ }
+ return "", nil
+}
+
+func fetchInstalledLicense(name, version string) string {
+ gemPaths := getGemPaths()
+ for _, dir := range gemPaths {
+ specsDir := filepath.Join(dir, "specifications")
+ // If version is specific
+ if version != "" && rubyVersionRe.MatchString(version) {
+ path := filepath.Join(specsDir,
name+"-"+version+".gemspec")
+ if _, license, err := parseGemspecInfo(path); err ==
nil && license != "" {
+ return license
+ }
+ } else {
+ // Scan for any version
+ entries, err := os.ReadDir(specsDir)
+ if err != nil {
+ continue
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasPrefix(e.Name(),
name+"-") || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ stem := strings.TrimSuffix(e.Name(), ".gemspec")
+ ver := strings.TrimPrefix(stem, name+"-")
+ if ver == stem { // didn't have prefix
+ continue
+ }
Review Comment:
The condition `if ver == stem` on line 525 will never be true because the
filename has already been validated to have the prefix `name+"-"` on line 520.
This redundant check can be removed for clarity. The subsequent check on line
529 already handles the case where the version doesn't start with a digit,
which is the actual validation needed.
```suggestion
```
##########
pkg/deps/ruby.go:
##########
@@ -116,12 +143,109 @@ func (r *GemfileLockResolver) Resolve(lockfile string,
config *ConfigDeps, repor
return nil
}
+// GemspecResolver resolves dependencies from a .gemspec file.
+// It extracts runtime dependencies defined in the gemspec and recursively
resolves
+// their transitive dependencies by looking up installed gems in the local
environment.
+type GemspecResolver struct {
+ Resolver
+}
+
+// CanResolve checks if the given file is a .gemspec file.
+func (r *GemspecResolver) CanResolve(file string) bool {
+ return strings.HasSuffix(file, ".gemspec")
+}
+
+// Resolve parses the gemspec file, identifies runtime dependencies, and
resolves
+// them along with their transitive dependencies. It reports the found
dependencies
+// and their licenses.
+func (r *GemspecResolver) Resolve(file string, config *ConfigDeps, report
*Report) error {
+ f, err := os.Open(file)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ deps := make(map[string]string) // name -> version constraint
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimLeft := strings.TrimLeft(line, " \t")
+ if strings.HasPrefix(trimLeft, "#") {
+ continue
+ }
+ if m := gemspecRuntimeRe.FindStringSubmatch(line); len(m) == 2 {
+ // NOTE: Version constraints are currently ignored. We
resolve to the first found installed version of the gem.
+ // This may lead to incorrect resolution if multiple
versions are installed and the first one doesn't satisfy the constraint.
+ deps[m[1]] = ""
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ // Recursive resolution
+ queue := make([]string, 0, len(deps))
+ visited := make(map[string]struct{}, len(deps))
+ for name := range deps {
+ queue = append(queue, name)
+ visited[name] = struct{}{}
+ }
+
+ for i := 0; i < len(queue); i++ {
+ name := queue[i]
+ // Find installed gemspec for 'name'
+ path, err := findInstalledGemspec(name, "")
+ if err == nil && path != "" {
+ // Parse dependencies of this gemspec
+ newDeps, err := parseGemspecDependencies(path)
+ if err == nil {
+ for _, dep := range newDeps {
+ if _, ok := visited[dep]; !ok {
+ if len(queue) > 10000 {
Review Comment:
The recursive dependency resolution uses a simple length check to prevent
infinite loops, but checking `len(queue) > 10000` after adding to the queue
means the queue could reach 10001 elements. The check should be performed
before adding to prevent exceeding the limit, or the error message should
reflect that the limit may be slightly exceeded.
```suggestion
if len(queue) >= 10000 {
```
##########
pkg/deps/ruby.go:
##########
@@ -253,6 +416,193 @@ func runtimeDepsFromGemspecs(dir string) ([]string,
error) {
return res, nil
}
+func parseGemspecDependencies(path string) ([]string, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ var deps []string
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimLeft := strings.TrimLeft(line, " \t")
+ if strings.HasPrefix(trimLeft, "#") {
+ continue
+ }
+ if m := gemspecRuntimeRe.FindStringSubmatch(line); len(m) == 2 {
+ deps = append(deps, m[1])
+ }
+ }
+ return deps, scanner.Err()
+}
+
+func findInstalledGemspec(name, version string) (string, error) {
+ gemPaths := getGemPaths()
+ for _, dir := range gemPaths {
+ specsDir := filepath.Join(dir, "specifications")
+ if version != "" && rubyVersionRe.MatchString(version) {
+ path := filepath.Join(specsDir,
name+"-"+version+".gemspec")
+ if _, err := os.Stat(path); err == nil {
+ return path, nil
+ }
+ } else {
+ entries, err := os.ReadDir(specsDir)
+ if err != nil {
+ continue
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasPrefix(e.Name(),
name+"-") || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ stem := strings.TrimSuffix(e.Name(), ".gemspec")
+ // Ensure that the character immediately after
the "name-" prefix
+ // is a digit, so we only consider filenames
where the suffix is
+ // a version component (e.g.,
"foo-1.0.0.gemspec") and avoid
+ // similar names like "foo-bar-1.0.0.gemspec"
when searching for "foo".
+ if len(stem) <= len(name)+1 {
+ continue
+ }
+ versionStart := stem[len(name)+1]
+ if versionStart < '0' || versionStart > '9' {
+ continue
+ }
+ ver := strings.TrimPrefix(stem, name+"-")
+ if ver == stem {
+ continue
+ }
Review Comment:
The condition `if ver == stem` on line 472 will never be true because the
function already verified that the filename has the prefix `name+"-"` earlier
(line 456), and the subsequent checks ensure the prefix is valid. This is dead
code that can be removed to improve code clarity.
```suggestion
```
##########
pkg/deps/ruby.go:
##########
@@ -116,12 +143,109 @@ func (r *GemfileLockResolver) Resolve(lockfile string,
config *ConfigDeps, repor
return nil
}
+// GemspecResolver resolves dependencies from a .gemspec file.
+// It extracts runtime dependencies defined in the gemspec and recursively
resolves
+// their transitive dependencies by looking up installed gems in the local
environment.
+type GemspecResolver struct {
+ Resolver
+}
+
+// CanResolve checks if the given file is a .gemspec file.
+func (r *GemspecResolver) CanResolve(file string) bool {
+ return strings.HasSuffix(file, ".gemspec")
+}
+
+// Resolve parses the gemspec file, identifies runtime dependencies, and
resolves
+// them along with their transitive dependencies. It reports the found
dependencies
+// and their licenses.
+func (r *GemspecResolver) Resolve(file string, config *ConfigDeps, report
*Report) error {
+ f, err := os.Open(file)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ deps := make(map[string]string) // name -> version constraint
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimLeft := strings.TrimLeft(line, " \t")
+ if strings.HasPrefix(trimLeft, "#") {
+ continue
+ }
+ if m := gemspecRuntimeRe.FindStringSubmatch(line); len(m) == 2 {
+ // NOTE: Version constraints are currently ignored. We
resolve to the first found installed version of the gem.
+ // This may lead to incorrect resolution if multiple
versions are installed and the first one doesn't satisfy the constraint.
+ deps[m[1]] = ""
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ // Recursive resolution
+ queue := make([]string, 0, len(deps))
+ visited := make(map[string]struct{}, len(deps))
+ for name := range deps {
+ queue = append(queue, name)
+ visited[name] = struct{}{}
+ }
+
+ for i := 0; i < len(queue); i++ {
+ name := queue[i]
+ // Find installed gemspec for 'name'
+ path, err := findInstalledGemspec(name, "")
+ if err == nil && path != "" {
+ // Parse dependencies of this gemspec
+ newDeps, err := parseGemspecDependencies(path)
+ if err == nil {
+ for _, dep := range newDeps {
+ if _, ok := visited[dep]; !ok {
+ if len(queue) > 10000 {
+ return
fmt.Errorf("dependency graph too large")
Review Comment:
The error message "dependency graph too large" is vague and doesn't provide
actionable information to the user. Consider enhancing the message to include
the current queue size and suggest potential causes, such as: "dependency graph
exceeded maximum size of 10000 nodes (current: %d). This may indicate a
circular dependency or an unusually large dependency tree."
```suggestion
return
fmt.Errorf("dependency graph exceeded maximum size of 10000 nodes (current:
%d). This may indicate a circular dependency or an unusually large dependency
tree", len(queue))
```
##########
pkg/deps/ruby.go:
##########
@@ -116,12 +143,109 @@ func (r *GemfileLockResolver) Resolve(lockfile string,
config *ConfigDeps, repor
return nil
}
+// GemspecResolver resolves dependencies from a .gemspec file.
+// It extracts runtime dependencies defined in the gemspec and recursively
resolves
+// their transitive dependencies by looking up installed gems in the local
environment.
+type GemspecResolver struct {
+ Resolver
+}
+
+// CanResolve checks if the given file is a .gemspec file.
+func (r *GemspecResolver) CanResolve(file string) bool {
+ return strings.HasSuffix(file, ".gemspec")
+}
+
+// Resolve parses the gemspec file, identifies runtime dependencies, and
resolves
+// them along with their transitive dependencies. It reports the found
dependencies
+// and their licenses.
+func (r *GemspecResolver) Resolve(file string, config *ConfigDeps, report
*Report) error {
+ f, err := os.Open(file)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ deps := make(map[string]string) // name -> version constraint
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimLeft := strings.TrimLeft(line, " \t")
+ if strings.HasPrefix(trimLeft, "#") {
+ continue
+ }
+ if m := gemspecRuntimeRe.FindStringSubmatch(line); len(m) == 2 {
+ // NOTE: Version constraints are currently ignored. We
resolve to the first found installed version of the gem.
+ // This may lead to incorrect resolution if multiple
versions are installed and the first one doesn't satisfy the constraint.
+ deps[m[1]] = ""
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ // Recursive resolution
+ queue := make([]string, 0, len(deps))
+ visited := make(map[string]struct{}, len(deps))
+ for name := range deps {
+ queue = append(queue, name)
+ visited[name] = struct{}{}
+ }
+
+ for i := 0; i < len(queue); i++ {
+ name := queue[i]
+ // Find installed gemspec for 'name'
+ path, err := findInstalledGemspec(name, "")
+ if err == nil && path != "" {
+ // Parse dependencies of this gemspec
+ newDeps, err := parseGemspecDependencies(path)
+ if err == nil {
+ for _, dep := range newDeps {
+ if _, ok := visited[dep]; !ok {
+ if len(queue) > 10000 {
+ return
fmt.Errorf("dependency graph too large")
+ }
+ visited[dep] = struct{}{}
+ queue = append(queue, dep)
+ if _, ok := deps[dep]; !ok {
+ deps[dep] = ""
+ }
+ }
+ }
Review Comment:
In the BFS loop for recursive dependency resolution, when
`findInstalledGemspec` or `parseGemspecDependencies` returns an error, the
error is silently ignored (lines 197-215). This means if there's an issue
reading a gemspec file (e.g., permission denied, corrupted file), the
transitive dependencies from that gem will be silently skipped without any
logging or warning. Consider adding debug logging when these operations fail to
help with troubleshooting incomplete dependency graphs.
```suggestion
if err != nil {
logrus.WithError(err).Debugf("failed to find installed
gemspec for gem %q", name)
continue
}
if path == "" {
logrus.Debugf("no installed gemspec found for gem %q",
name)
continue
}
// Parse dependencies of this gemspec
newDeps, err := parseGemspecDependencies(path)
if err != nil {
logrus.WithError(err).Debugf("failed to parse gemspec
dependencies from %q for gem %q", path, name)
continue
}
for _, dep := range newDeps {
if _, ok := visited[dep]; !ok {
if len(queue) > 10000 {
return fmt.Errorf("dependency graph too
large")
}
visited[dep] = struct{}{}
queue = append(queue, dep)
if _, ok := deps[dep]; !ok {
deps[dep] = ""
}
```
##########
pkg/deps/ruby.go:
##########
@@ -253,6 +416,193 @@ func runtimeDepsFromGemspecs(dir string) ([]string,
error) {
return res, nil
}
+func parseGemspecDependencies(path string) ([]string, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ var deps []string
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimLeft := strings.TrimLeft(line, " \t")
+ if strings.HasPrefix(trimLeft, "#") {
+ continue
+ }
+ if m := gemspecRuntimeRe.FindStringSubmatch(line); len(m) == 2 {
+ deps = append(deps, m[1])
+ }
+ }
+ return deps, scanner.Err()
+}
+
+func findInstalledGemspec(name, version string) (string, error) {
+ gemPaths := getGemPaths()
+ for _, dir := range gemPaths {
+ specsDir := filepath.Join(dir, "specifications")
+ if version != "" && rubyVersionRe.MatchString(version) {
+ path := filepath.Join(specsDir,
name+"-"+version+".gemspec")
+ if _, err := os.Stat(path); err == nil {
+ return path, nil
+ }
+ } else {
+ entries, err := os.ReadDir(specsDir)
+ if err != nil {
+ continue
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasPrefix(e.Name(),
name+"-") || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ stem := strings.TrimSuffix(e.Name(), ".gemspec")
+ // Ensure that the character immediately after
the "name-" prefix
+ // is a digit, so we only consider filenames
where the suffix is
+ // a version component (e.g.,
"foo-1.0.0.gemspec") and avoid
+ // similar names like "foo-bar-1.0.0.gemspec"
when searching for "foo".
+ if len(stem) <= len(name)+1 {
+ continue
+ }
+ versionStart := stem[len(name)+1]
+ if versionStart < '0' || versionStart > '9' {
+ continue
+ }
+ ver := strings.TrimPrefix(stem, name+"-")
+ if ver == stem {
+ continue
+ }
+ path := filepath.Join(specsDir, e.Name())
+ if specName, _, err := parseGemspecInfo(path);
err == nil && specName == name {
+ return path, nil
+ }
+ }
+ }
+ }
+ return "", os.ErrNotExist
+}
+
+func fetchLocalLicense(dir, targetName string) (string, error) {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return "", err
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ path := filepath.Join(dir, e.Name())
+ specName, license, err := parseGemspecInfo(path)
+ if err == nil && specName == targetName && license != "" {
+ return license, nil
+ }
+ }
+ return "", nil
+}
+
+func fetchInstalledLicense(name, version string) string {
+ gemPaths := getGemPaths()
+ for _, dir := range gemPaths {
+ specsDir := filepath.Join(dir, "specifications")
+ // If version is specific
+ if version != "" && rubyVersionRe.MatchString(version) {
+ path := filepath.Join(specsDir,
name+"-"+version+".gemspec")
+ if _, license, err := parseGemspecInfo(path); err ==
nil && license != "" {
+ return license
+ }
+ } else {
+ // Scan for any version
+ entries, err := os.ReadDir(specsDir)
+ if err != nil {
+ continue
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasPrefix(e.Name(),
name+"-") || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ stem := strings.TrimSuffix(e.Name(), ".gemspec")
+ ver := strings.TrimPrefix(stem, name+"-")
+ if ver == stem { // didn't have prefix
+ continue
+ }
+ // Ensure the character after the gem name
corresponds to the start of a version
+ if ver == "" || ver[0] < '0' || ver[0] > '9' {
+ continue
+ }
+ path := filepath.Join(specsDir, e.Name())
+ if specName, license, err :=
parseGemspecInfo(path); err == nil && specName == name && license != "" {
+ return license
+ }
+ }
Review Comment:
The `fetchInstalledLicense` function performs multiple directory scans when
a version is not specified. For each gem path in `GEM_PATH`, it reads all
entries in the specifications directory and iterates through them (lines
515-536). If `GEM_PATH` contains multiple directories with many gems, this
could lead to significant I/O overhead. Consider caching the results or using a
more efficient lookup strategy, such as building an index of available gems on
first access.
##########
pkg/deps/ruby.go:
##########
@@ -253,6 +416,193 @@ func runtimeDepsFromGemspecs(dir string) ([]string,
error) {
return res, nil
}
+func parseGemspecDependencies(path string) ([]string, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ var deps []string
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimLeft := strings.TrimLeft(line, " \t")
+ if strings.HasPrefix(trimLeft, "#") {
+ continue
+ }
+ if m := gemspecRuntimeRe.FindStringSubmatch(line); len(m) == 2 {
+ deps = append(deps, m[1])
+ }
+ }
+ return deps, scanner.Err()
+}
+
+func findInstalledGemspec(name, version string) (string, error) {
+ gemPaths := getGemPaths()
+ for _, dir := range gemPaths {
+ specsDir := filepath.Join(dir, "specifications")
+ if version != "" && rubyVersionRe.MatchString(version) {
+ path := filepath.Join(specsDir,
name+"-"+version+".gemspec")
+ if _, err := os.Stat(path); err == nil {
+ return path, nil
+ }
+ } else {
+ entries, err := os.ReadDir(specsDir)
+ if err != nil {
+ continue
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasPrefix(e.Name(),
name+"-") || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ stem := strings.TrimSuffix(e.Name(), ".gemspec")
+ // Ensure that the character immediately after
the "name-" prefix
+ // is a digit, so we only consider filenames
where the suffix is
+ // a version component (e.g.,
"foo-1.0.0.gemspec") and avoid
+ // similar names like "foo-bar-1.0.0.gemspec"
when searching for "foo".
+ if len(stem) <= len(name)+1 {
+ continue
+ }
+ versionStart := stem[len(name)+1]
+ if versionStart < '0' || versionStart > '9' {
+ continue
+ }
+ ver := strings.TrimPrefix(stem, name+"-")
+ if ver == stem {
+ continue
+ }
+ path := filepath.Join(specsDir, e.Name())
+ if specName, _, err := parseGemspecInfo(path);
err == nil && specName == name {
+ return path, nil
+ }
+ }
+ }
+ }
+ return "", os.ErrNotExist
+}
+
+func fetchLocalLicense(dir, targetName string) (string, error) {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return "", err
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ path := filepath.Join(dir, e.Name())
+ specName, license, err := parseGemspecInfo(path)
+ if err == nil && specName == targetName && license != "" {
+ return license, nil
+ }
+ }
+ return "", nil
+}
+
+func fetchInstalledLicense(name, version string) string {
+ gemPaths := getGemPaths()
+ for _, dir := range gemPaths {
+ specsDir := filepath.Join(dir, "specifications")
+ // If version is specific
+ if version != "" && rubyVersionRe.MatchString(version) {
+ path := filepath.Join(specsDir,
name+"-"+version+".gemspec")
+ if _, license, err := parseGemspecInfo(path); err ==
nil && license != "" {
+ return license
+ }
+ } else {
+ // Scan for any version
+ entries, err := os.ReadDir(specsDir)
+ if err != nil {
+ continue
+ }
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasPrefix(e.Name(),
name+"-") || !strings.HasSuffix(e.Name(), ".gemspec") {
+ continue
+ }
+ stem := strings.TrimSuffix(e.Name(), ".gemspec")
+ ver := strings.TrimPrefix(stem, name+"-")
+ if ver == stem { // didn't have prefix
+ continue
+ }
+ // Ensure the character after the gem name
corresponds to the start of a version
+ if ver == "" || ver[0] < '0' || ver[0] > '9' {
+ continue
+ }
+ path := filepath.Join(specsDir, e.Name())
+ if specName, license, err :=
parseGemspecInfo(path); err == nil && specName == name && license != "" {
+ return license
+ }
+ }
+ }
+ }
+ return ""
+}
Review Comment:
The new `fetchInstalledLicense` function is called in both resolvers but is
not directly tested in unit tests. While it's exercised through integration
tests, adding specific test cases for this function would improve coverage and
make it easier to verify edge cases like:
- Multiple versions of the same gem installed
- Invalid version strings
- Missing gemspec files
- Gems with similar names (e.g., "foo" vs "foo-bar")
##########
pkg/deps/ruby.go:
##########
@@ -116,12 +143,109 @@ func (r *GemfileLockResolver) Resolve(lockfile string,
config *ConfigDeps, repor
return nil
}
+// GemspecResolver resolves dependencies from a .gemspec file.
+// It extracts runtime dependencies defined in the gemspec and recursively
resolves
+// their transitive dependencies by looking up installed gems in the local
environment.
+type GemspecResolver struct {
+ Resolver
+}
+
+// CanResolve checks if the given file is a .gemspec file.
+func (r *GemspecResolver) CanResolve(file string) bool {
+ return strings.HasSuffix(file, ".gemspec")
+}
+
+// Resolve parses the gemspec file, identifies runtime dependencies, and
resolves
+// them along with their transitive dependencies. It reports the found
dependencies
+// and their licenses.
+func (r *GemspecResolver) Resolve(file string, config *ConfigDeps, report
*Report) error {
+ f, err := os.Open(file)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ deps := make(map[string]string) // name -> version constraint
+ for scanner.Scan() {
+ line := scanner.Text()
+ trimLeft := strings.TrimLeft(line, " \t")
+ if strings.HasPrefix(trimLeft, "#") {
+ continue
+ }
+ if m := gemspecRuntimeRe.FindStringSubmatch(line); len(m) == 2 {
+ // NOTE: Version constraints are currently ignored. We
resolve to the first found installed version of the gem.
+ // This may lead to incorrect resolution if multiple
versions are installed and the first one doesn't satisfy the constraint.
+ deps[m[1]] = ""
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+
+ // Recursive resolution
+ queue := make([]string, 0, len(deps))
+ visited := make(map[string]struct{}, len(deps))
+ for name := range deps {
+ queue = append(queue, name)
+ visited[name] = struct{}{}
+ }
+
+ for i := 0; i < len(queue); i++ {
+ name := queue[i]
+ // Find installed gemspec for 'name'
+ path, err := findInstalledGemspec(name, "")
+ if err == nil && path != "" {
+ // Parse dependencies of this gemspec
+ newDeps, err := parseGemspecDependencies(path)
+ if err == nil {
+ for _, dep := range newDeps {
+ if _, ok := visited[dep]; !ok {
+ if len(queue) > 10000 {
+ return
fmt.Errorf("dependency graph too large")
+ }
Review Comment:
The BFS dependency graph size limit check (line 204) that prevents infinite
loops is not tested. Adding a test case with a circular dependency or a very
deep dependency tree would verify that the limit works correctly and produces
an appropriate error message.
##########
pkg/deps/ruby.go:
##########
@@ -104,7 +108,30 @@ func (r *GemfileLockResolver) Resolve(lockfile string,
config *ConfigDeps, repor
continue
}
- licenseID, err := fetchRubyGemsLicense(name, version)
+ if localPath != "" {
+ baseDir, err := filepath.Abs(dir)
+ if err != nil {
+ logrus.WithError(err).Warn("failed to resolve
base directory for local gem path")
+ } else {
+ candidatePath :=
filepath.Clean(filepath.Join(baseDir, localPath))
+ if candidatePath == baseDir ||
strings.HasPrefix(candidatePath, baseDir+string(os.PathSeparator)) {
+ fullPath := candidatePath
+ license, err :=
fetchLocalLicense(fullPath, name)
+ if err == nil && license != "" {
+
report.Resolve(&Result{Dependency: name, LicenseSpdxID: license, Version:
version})
+ continue
+ }
+ } else {
+ logrus.WithField("path",
localPath).Warn("ignoring potentially unsafe local gem path outside project
directory")
+ }
+ }
Review Comment:
The path traversal protection logic (lines 112-127) is not covered by tests.
While the local dependency test case exercises local path resolution, it
doesn't test edge cases like:
- Path traversal attempts (e.g., `path: '../../../etc'`)
- Symbolic link handling
- Absolute paths in the `remote:` field
- Paths with special characters or spaces
Adding test cases for these security-sensitive scenarios would ensure the
protection logic works correctly.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]