Skip to content

Commit 4b226da

Browse files
Merge pull request #6 from Checkmarx/feature/elchanan/manifest-parser-dotnet
Implement parsing for .NET package formats (AST-95487)
2 parents 1c39081 + e1af049 commit 4b226da

15 files changed

+828
-150
lines changed

internal/helper.go

Lines changed: 0 additions & 30 deletions
This file was deleted.

internal/parsers/dotnet/csproj_parser.go

Lines changed: 129 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,164 @@ package dotnet
22

33
import (
44
"encoding/xml"
5+
"fmt"
56
"io"
67
"os"
8+
"regexp"
79
"strings"
810

911
"github.com/Checkmarx/manifest-parser/pkg/parser/models"
1012
)
1113

14+
// DotnetCsprojParser implements parsing of .NET project files (.csproj)
1215
type DotnetCsprojParser struct{}
1316

17+
// PackageReference represents a package reference in the .csproj file
1418
type PackageReference struct {
15-
Include string `xml:"Include,attr"`
16-
Version string `xml:"Version,attr"`
19+
Include string `xml:"Include,attr"`
20+
VersionAttr string `xml:"Version,attr"`
21+
VersionNested string `xml:"Version"`
1722
}
1823

24+
// PackageReferenceTag is the XML tag for package references in .csproj files
25+
const PackageReferenceTag = "PackageReference"
26+
27+
// parseVersion handles version resolution
28+
// - Returns exact version if specified
29+
// - Returns "latest" for version ranges or special version specifiers
30+
func parseVersion(version string) string {
31+
// Handle empty version
32+
if version == "" {
33+
return "latest"
34+
}
35+
36+
// If the version contains any kind of brackets, return "latest"
37+
if strings.ContainsAny(version, "[]()") {
38+
return "latest"
39+
}
40+
41+
// Handle special version specifiers
42+
if strings.ContainsAny(version, "*^~><") {
43+
return "latest"
44+
}
45+
46+
// Return exact version
47+
return version
48+
}
49+
50+
// computeIndices calculates start and end indices for PackageReference elements
51+
// Returns startIndex and endIndex for the element in the line
52+
func computeIndices(lines []string, lineNum int) (startIndex, endIndex int, lineStart, lineEnd int) {
53+
currentLine := lines[lineNum-1] // lineNum is 1-based so we subtract 1
54+
55+
// Find the position of the PackageReference tag start in the line
56+
startIdx := strings.Index(currentLine, "<PackageReference")
57+
if startIdx < 0 {
58+
return 1, len(currentLine), lineNum, lineNum
59+
}
60+
61+
// Check if it's a single-line format
62+
if strings.Contains(currentLine, "/>") {
63+
// Single-line format
64+
endIdx := strings.LastIndex(currentLine, "/>") + 2 // Include the "/>" itself
65+
return startIdx + 1, endIdx + 1, lineNum, lineNum
66+
}
67+
68+
// Multi-line format
69+
// TODO: Multi-line PackageReference support will be handled in the future.
70+
// Currently, if the tag spans multiple lines, we only return the first line.
71+
// The following code is commented out for now:
72+
/*
73+
lineEnd = lineNum
74+
for i := lineNum; i < len(lines) && i < lineNum+10; i++ { // Limit search to 10 lines
75+
if strings.Contains(lines[i-1], "</PackageReference>") {
76+
lineEnd = i
77+
endLine := lines[i-1]
78+
endIdx := strings.Index(endLine, "</PackageReference>") + len("</PackageReference>")
79+
return startIdx + 1, endIdx + 1, lineNum, lineEnd
80+
}
81+
}
82+
*/
83+
// No closing tag found, return the end of the current line
84+
return startIdx + 1, len(currentLine) + 1, lineNum, lineNum
85+
}
86+
87+
// Parse implements the Parser interface for .csproj files
1988
func (p *DotnetCsprojParser) Parse(manifestFile string) ([]models.Package, error) {
89+
// Read the file content
2090
content, err := os.ReadFile(manifestFile)
2191
if err != nil {
22-
return nil, err
92+
return nil, fmt.Errorf("failed to read manifest file: %w", err)
2393
}
2494

25-
decoder := xml.NewDecoder(strings.NewReader(string(content)))
95+
// Split content into lines for index computation
96+
strContent := string(content)
97+
lines := strings.Split(strContent, "\n")
98+
99+
// Create XML decoder
100+
decoder := xml.NewDecoder(strings.NewReader(strContent))
26101
var packages []models.Package
27-
var currentElement *PackageReference
28102

103+
// Parse XML content
29104
for {
30-
tok, err := decoder.Token()
105+
token, err := decoder.Token()
31106
if err != nil {
32107
if err == io.EOF {
33108
break
34109
}
35-
return nil, err
110+
return nil, fmt.Errorf("failed to parse XML: %w", err)
36111
}
37112

38-
switch elem := tok.(type) {
113+
// Process each element
114+
switch elem := token.(type) {
39115
case xml.StartElement:
40-
if elem.Name.Local == "PackageReference" {
41-
currentElement = &PackageReference{}
42-
err := decoder.DecodeElement(currentElement, &elem)
43-
if err != nil {
44-
return nil, err
116+
if elem.Name.Local == PackageReferenceTag {
117+
var pkgRef PackageReference
118+
if err := decoder.DecodeElement(&pkgRef, &elem); err != nil {
119+
return nil, fmt.Errorf("failed to decode PackageReference: %w", err)
45120
}
46-
line, _ := decoder.InputPos()
121+
122+
// Skip empty package names
123+
if pkgRef.Include == "" {
124+
continue
125+
}
126+
127+
// Find line number
128+
lineNum := 0
129+
packagePattern := fmt.Sprintf(`PackageReference.*Include="%s"`, pkgRef.Include)
130+
re := regexp.MustCompile(packagePattern)
131+
132+
for i, line := range lines {
133+
if re.MatchString(line) {
134+
lineNum = i + 1 // 1-indexed line numbers
135+
break
136+
}
137+
}
138+
139+
// Skip if line not found
140+
if lineNum == 0 {
141+
continue
142+
}
143+
144+
// Compute indices for both single-line and multi-line formats
145+
startCol, endCol, lineStart, lineEnd := computeIndices(lines, lineNum)
146+
147+
// Determine the version
148+
version := pkgRef.VersionAttr
149+
if version == "" {
150+
version = pkgRef.VersionNested
151+
}
152+
153+
// Create package entry
47154
packages = append(packages, models.Package{
48-
PackageName: currentElement.Include,
49-
Version: currentElement.Version,
50-
LineStart: line,
51-
LineEnd: line,
52-
Filepath: manifestFile,
155+
PackageManager: "dotnet",
156+
PackageName: pkgRef.Include,
157+
Version: parseVersion(version),
158+
Filepath: manifestFile,
159+
LineStart: lineStart,
160+
LineEnd: lineEnd,
161+
StartIndex: startCol,
162+
EndIndex: endCol,
53163
})
54164
}
55165
}

0 commit comments

Comments
 (0)