Copilot commented on code in PR #255:
URL: https://github.com/apache/skywalking-eyes/pull/255#discussion_r2635022809


##########
pkg/deps/ruby.go:
##########
@@ -136,68 +240,111 @@ func parseGemfileLock(s string) (graph gemGraph, roots 
[]string, err error) {
        scanner.Split(bufio.ScanLines)
        graph = make(gemGraph)
 
-       inSpecs := false
-       inDeps := false
-       var current *gemSpec
+       state := &lockParserState{
+               graph: graph,
+       }
 
        for scanner.Scan() {
-               line := scanner.Text()
-               if strings.HasPrefix(line, "GEM") {
-                       inSpecs = true
-                       inDeps = false
-                       current = nil
-                       continue
-               }
-               if strings.HasPrefix(line, "DEPENDENCIES") {
-                       inSpecs = false
-                       inDeps = true
-                       current = nil
-                       continue
-               }
-               if strings.TrimSpace(line) == "specs:" && inSpecs {
-                       // just a marker
-                       continue
-               }
+               state.processLine(scanner.Text())
+       }
+       if err := scanner.Err(); err != nil {
+               return nil, nil, err
+       }
+       return graph, state.roots, nil
+}
 
-               if inSpecs {
-                       if m := lockSpecHeader.FindStringSubmatch(line); len(m) 
== 3 {
-                               name := m[1]
-                               version := m[2]
-                               current = &gemSpec{Name: name, Version: version}
-                               graph[name] = current
-                               continue
-                       }
-                       if current != nil {
-                               if m := lockDepLine.FindStringSubmatch(line); 
len(m) == 2 {
-                                       depName := m[1]
-                                       current.Deps = append(current.Deps, 
depName)
-                               }
-                       }
-                       continue
+type lockParserState struct {
+       inSpecs           bool
+       inDeps            bool
+       inPath            bool
+       current           *gemSpec
+       currentRemotePath string
+       graph             gemGraph
+       roots             []string
+}
+
+func (s *lockParserState) processLine(line string) {
+       if strings.HasPrefix(line, "GEM") {
+               s.inSpecs = true
+               s.inDeps = false
+               s.inPath = false
+               s.currentRemotePath = ""
+               s.current = nil
+               return
+       }
+       if strings.HasPrefix(line, "PATH") {
+               s.inSpecs = true
+               s.inDeps = false
+               s.inPath = true
+               s.current = nil
+               return
+       }
+       if strings.HasPrefix(line, "DEPENDENCIES") {
+               s.inSpecs = false
+               s.inDeps = true
+               s.inPath = false
+               s.current = nil
+               return
+       }
+       if strings.TrimSpace(line) == "specs:" && s.inSpecs {
+               // just a marker
+               return
+       }
+
+       if s.inSpecs {
+               s.processSpecs(line)
+               return
+       }
+
+       if s.inDeps {
+               s.processDeps(line)
+               return
+       }
+}
+
+func (s *lockParserState) processSpecs(line string) {
+       trim := strings.TrimSpace(line)
+       if strings.HasPrefix(trim, "remote:") {
+               if s.inPath {
+                       s.currentRemotePath = 
strings.TrimSpace(strings.TrimPrefix(trim, "remote:"))
                }

Review Comment:
   Consider adding a comment explaining that the `inPath` check ensures that 
only PATH block remote paths are captured, not GEM block remote URLs (like 
https://rubygems.org/). This distinction is important for proper local 
dependency resolution.



##########
pkg/deps/ruby.go:
##########
@@ -253,6 +390,157 @@ 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 != "" && !strings.ContainsAny(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") {

Review Comment:
   The condition checking `strings.HasPrefix(e.Name(), name+"-")` could produce 
false positives when gem names are substrings of other gem names. For example, 
searching for "foo" would also match "foo-bar-1.0.0.gemspec". Consider using a 
more precise check that validates the character after the name prefix is a dash 
followed by a version number, or matches the exact pattern 
"name-version.gemspec".



##########
pkg/deps/ruby.go:
##########
@@ -214,6 +361,8 @@ func hasGemspec(dir string) bool {
 }
 
 var gemspecRuntimeRe = 
regexp.MustCompile(`\badd_(?:runtime_)?dependency\s*\(?\s*["']([^"']+)["']`)

Review Comment:
   The regex pattern `\.licenses?\s*=\s*(?:\[\s*)?['"]([^'"]+)['"]` only 
captures the first license when multiple licenses are specified in an array 
(e.g., `s.licenses = ['MIT', 'Apache-2.0']`). The pattern should be improved to 
handle multiple licenses in arrays or at least document this limitation. 
Consider capturing all licenses within array brackets or using a different 
parsing approach for arrays.
   ```suggestion
   var gemspecRuntimeRe = 
regexp.MustCompile(`\badd_(?:runtime_)?dependency\s*\(?\s*["']([^"']+)["']`)
   // Note: this pattern only captures the first license when multiple licenses 
are specified in an array.
   ```



##########
pkg/deps/ruby.go:
##########
@@ -253,6 +390,157 @@ 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 != "" && !strings.ContainsAny(version, "<>~=") {

Review Comment:
   The version constraint check `!strings.ContainsAny(version, "<>~=")` is 
incomplete. Ruby version constraints can also use operators like `!=` (not 
equal) and `.pre` suffixes for pre-release versions. A version string like 
"1.0.0.pre" would incorrectly be treated as a specific version. Consider using 
a more robust check or regex pattern to validate version numbers.
   ```suggestion
                if version != "" && !strings.ContainsAny(version, "<>~=!") && 
!strings.Contains(version, ".pre") {
   ```



##########
pkg/deps/ruby.go:
##########
@@ -253,6 +390,157 @@ 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 != "" && !strings.ContainsAny(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") {
+                                       stem := strings.TrimSuffix(e.Name(), 
".gemspec")
+                                       ver := strings.TrimPrefix(stem, 
name+"-")
+                                       if ver == stem {
+                                               continue
+                                       }
+                                       return filepath.Join(specsDir, 
e.Name()), 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 != "" && !strings.ContainsAny(version, "<>~=") { // 
simple check if it's a version number

Review Comment:
   The version constraint check `!strings.ContainsAny(version, "<>~=")` is 
incomplete. Ruby version constraints can also use operators like `!=` (not 
equal) and `.pre` suffixes for pre-release versions. A version string like 
"1.0.0.pre" would incorrectly be treated as a specific version. Consider using 
a more robust check or regex pattern to validate version numbers.



##########
pkg/deps/ruby.go:
##########
@@ -253,6 +390,157 @@ 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 != "" && !strings.ContainsAny(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") {
+                                       stem := strings.TrimSuffix(e.Name(), 
".gemspec")
+                                       ver := strings.TrimPrefix(stem, 
name+"-")
+                                       if ver == stem {
+                                               continue
+                                       }
+                                       return filepath.Join(specsDir, 
e.Name()), 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 != "" && !strings.ContainsAny(version, "<>~=") { // 
simple check if it's a version number
+                       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
+                               }
+                               path := filepath.Join(specsDir, e.Name())
+                               if _, license, err := parseGemspecInfo(path); 
err == nil && license != "" {
+                                       return license
+                               }
+                       }
+               }
+       }
+       return ""
+}
+
+func getGemPaths() []string {
+       env := os.Getenv("GEM_PATH")
+       if env == "" {
+               env = os.Getenv("GEM_HOME")
+       }
+       if env == "" {
+               return nil
+       }
+       return strings.Split(env, string(os.PathListSeparator))
+}
+
+func parseGemspecInfo(path string) (string, string, error) {
+       f, err := os.Open(path)
+       if err != nil {
+               return "", "", err
+       }
+       defer f.Close()
+       scanner := bufio.NewScanner(f)
+       var name, license string
+       for scanner.Scan() {
+               line := scanner.Text()
+               trimLeft := strings.TrimLeft(line, " \t")
+               if strings.HasPrefix(trimLeft, "#") {
+                       continue
+               }
+               if name == "" {
+                       if m := gemspecNameRe.FindStringSubmatch(line); len(m) 
== 2 {
+                               name = m[1]
+                       }
+               }
+               if license == "" {
+                       if m := gemspecLicenseRe.FindStringSubmatch(line); 
len(m) == 2 {
+                               license = m[1]
+                       }
+               }
+               if name != "" && license != "" {
+                       break
+               }
+       }
+       return name, license, nil
+}
+
+func parseGemspecLicense(path string) (string, error) {
+       _, license, err := parseGemspecInfo(path)
+       return license, err
+}
+

Review Comment:
   The function `parseGemspecLicense` appears to be unused and simply wraps 
`parseGemspecInfo`. Consider removing this function if it's not used elsewhere 
in the codebase to reduce code duplication.
   ```suggestion
   
   ```



##########
pkg/deps/ruby.go:
##########
@@ -116,12 +131,101 @@ func (r *GemfileLockResolver) Resolve(lockfile string, 
config *ConfigDeps, repor
        return nil
 }
 
+// GemspecResolver resolves dependencies from a .gemspec file.
+type GemspecResolver struct {
+       Resolver
+}

Review Comment:
   The GemspecResolver struct and its Resolve method lack documentation 
comments explaining their purpose and behavior. Consider adding a doc comment 
describing that this resolver handles .gemspec files by extracting runtime 
dependencies and recursively resolving their transitive dependencies from 
installed gems.



##########
pkg/deps/ruby.go:
##########
@@ -116,12 +131,101 @@ func (r *GemfileLockResolver) Resolve(lockfile string, 
config *ConfigDeps, repor
        return nil
 }
 
+// GemspecResolver resolves dependencies from a .gemspec file.
+type GemspecResolver struct {
+       Resolver
+}
+
+func (r *GemspecResolver) CanResolve(file string) bool {
+       return strings.HasSuffix(file, ".gemspec")
+}
+
+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 {
+                       deps[m[1]] = "" // We don't extract version constraint 
yet, or we could improve regex
+               }
+       }
+       if err := scanner.Err(); err != nil {
+               return err
+       }
+
+       // Recursive resolution
+       queue := make([]string, 0, len(deps))
+       for name := range deps {
+               queue = append(queue, name)
+       }
+
+       visited := make(map[string]struct{})
+       for name := range deps {

Review Comment:
   The queue is initialized with all direct dependencies before the BFS loop 
begins. Consider removing the separate initialization loops at lines 167-174 
and instead initialize the queue and visited map in a single loop to reduce 
code duplication.
   ```suggestion
        visited := make(map[string]struct{}, len(deps))
        for name := range deps {
                queue = append(queue, name)
   ```



##########
pkg/deps/ruby.go:
##########
@@ -253,6 +390,157 @@ 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 != "" && !strings.ContainsAny(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") {
+                                       stem := strings.TrimSuffix(e.Name(), 
".gemspec")
+                                       ver := strings.TrimPrefix(stem, 
name+"-")
+                                       if ver == stem {
+                                               continue
+                                       }
+                                       return filepath.Join(specsDir, 
e.Name()), 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 != "" && !strings.ContainsAny(version, "<>~=") { // 
simple check if it's a version number
+                       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") {

Review Comment:
   The condition checking `strings.HasPrefix(e.Name(), name+"-")` could produce 
false positives when gem names are substrings of other gem names. For example, 
searching for "foo" would also match "foo-bar-1.0.0.gemspec". Consider using a 
more precise check that validates the character after the name prefix is a dash 
followed by a version number, or matches the exact pattern 
"name-version.gemspec".
   ```suggestion
                        re := regexp.MustCompile("^" + regexp.QuoteMeta(name) + 
"-[0-9].*\\.gemspec$")
                        for _, e := range entries {
                                if e.IsDir() || !re.MatchString(e.Name()) {
   ```



-- 
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]

Reply via email to