| | | 1 | | package repometa |
| | | 2 | | |
| | | 3 | | import ( |
| | | 4 | | "path/filepath" |
| | | 5 | | "regexp" |
| | | 6 | | "strings" |
| | | 7 | | ) |
| | | 8 | | |
| | | 9 | | type dotnetDetector struct{} |
| | | 10 | | |
| | | 11 | | // slnProjectLineRE matches the Project(...) header lines that .sln files |
| | | 12 | | // use to declare each member project. Format: |
| | | 13 | | // |
| | | 14 | | // Project("{TYPE-GUID}") = "Name", "relative/path/to/Name.csproj", "{PROJECT-GUID}" |
| | | 15 | | // |
| | | 16 | | // Only the second field (the path) is captured; the type GUID (which |
| | | 17 | | // distinguishes projects from solution folders) is inspected downstream |
| | | 18 | | // by the extension of the path itself. |
| | | 19 | | var slnProjectLineRE = regexp.MustCompile(`(?i)^Project\([^)]+\)\s*=\s*"[^"]*",\s*"([^"]+)"`) |
| | | 20 | | |
| | 84 | 21 | | func (dotnetDetector) detect(dv dirVisit, cfg options) []finding { |
| | 84 | 22 | | var out []finding |
| | 84 | 23 | | |
| | 84 | 24 | | for _, f := range dv.files { |
| | 0 | 25 | | if f.IsDir() { |
| | 0 | 26 | | continue |
| | | 27 | | } |
| | 85 | 28 | | name := f.Name() |
| | 85 | 29 | | ext := strings.ToLower(filepath.Ext(name)) |
| | 85 | 30 | | |
| | 85 | 31 | | switch ext { |
| | 3 | 32 | | case ".sln": |
| | 3 | 33 | | // A .sln is the Visual Studio solution format used across |
| | 3 | 34 | | // every MSBuild language (C#, F#, VB, and C++). We only |
| | 3 | 35 | | // emit dotnet-solution when the .sln references at least |
| | 3 | 36 | | // one .NET project file — a pure-C++ solution or one whose |
| | 3 | 37 | | // projects are all solution folders is not a .NET workspace |
| | 3 | 38 | | // and should not be labeled as such. The C++ projects it |
| | 3 | 39 | | // does contain still surface as cpp-project components at |
| | 3 | 40 | | // their own directories. |
| | 3 | 41 | | members := parseSlnMembers(filepath.Join(dv.abs, name), cfg) |
| | 2 | 42 | | if len(members) == 0 { |
| | 2 | 43 | | continue |
| | | 44 | | } |
| | 1 | 45 | | out = append(out, finding{ |
| | 1 | 46 | | Kind: KindDotNetSolution, |
| | 1 | 47 | | Confidence: 1.0, |
| | 1 | 48 | | Evidence: []Evidence{{ |
| | 1 | 49 | | Path: relJoin(dv.rel, name), |
| | 1 | 50 | | Reason: ".sln at directory root", |
| | 1 | 51 | | }}, |
| | 1 | 52 | | Workspaces: []Workspace{{ |
| | 1 | 53 | | Kind: WorkspaceDotNetSolution, |
| | 1 | 54 | | Members: expandMembers(dv.abs, members, dv.rel), |
| | 1 | 55 | | }}, |
| | 1 | 56 | | }) |
| | | 57 | | |
| | 5 | 58 | | case ".csproj", ".fsproj", ".vbproj": |
| | 5 | 59 | | out = append(out, finding{ |
| | 5 | 60 | | Kind: KindDotNetProject, |
| | 5 | 61 | | Confidence: 1.0, |
| | 5 | 62 | | Evidence: []Evidence{{ |
| | 5 | 63 | | Path: relJoin(dv.rel, name), |
| | 5 | 64 | | Reason: ext + " project file", |
| | 5 | 65 | | }}, |
| | 5 | 66 | | Attributes: map[string]string{ |
| | 5 | 67 | | "dotnet.language": dotnetLanguageFor(ext), |
| | 5 | 68 | | }, |
| | 5 | 69 | | }) |
| | | 70 | | |
| | 2 | 71 | | case ".vcxproj": |
| | 2 | 72 | | // Visual Studio C++ project. Shares the MSBuild machinery |
| | 2 | 73 | | // with .csproj but targets native C/C++ code, so it maps to |
| | 2 | 74 | | // LanguageC in the polyglot classifier and suppresses loose |
| | 2 | 75 | | // C-source detection inside the same directory (see |
| | 2 | 76 | | // isCBuildKind in scan.go). |
| | 2 | 77 | | out = append(out, finding{ |
| | 2 | 78 | | Kind: KindCppProject, |
| | 2 | 79 | | Confidence: 1.0, |
| | 2 | 80 | | Evidence: []Evidence{{ |
| | 2 | 81 | | Path: relJoin(dv.rel, name), |
| | 2 | 82 | | Reason: ".vcxproj project file", |
| | 2 | 83 | | }}, |
| | 2 | 84 | | }) |
| | | 85 | | } |
| | | 86 | | } |
| | 84 | 87 | | return out |
| | | 88 | | } |
| | | 89 | | |
| | | 90 | | // dotnetLanguageFor maps a project-file extension to the language label |
| | | 91 | | // exposed on the dotnet.language attribute. |
| | | 92 | | func dotnetLanguageFor(ext string) string { |
| | | 93 | | switch ext { |
| | | 94 | | case ".csproj": |
| | | 95 | | return "csharp" |
| | | 96 | | case ".fsproj": |
| | | 97 | | return "fsharp" |
| | | 98 | | case ".vbproj": |
| | | 99 | | return "vb" |
| | | 100 | | } |
| | | 101 | | return "" |
| | | 102 | | } |
| | | 103 | | |
| | | 104 | | // parseSlnMembers walks the Project(...) lines of a .sln file and returns |
| | | 105 | | // the directory-relative paths of every C#/F#/VB project it references. |
| | | 106 | | // Solution folders (identified by their type GUID) reuse the Project(...) |
| | | 107 | | // prefix but point at a virtual name rather than a real project file — |
| | | 108 | | // they are filtered out by extension so members line up with the |
| | | 109 | | // dotnet-project components emitted by the walker. |
| | | 110 | | // |
| | | 111 | | // .sln paths are Windows-style with backslashes; they are normalized to |
| | | 112 | | // forward slashes for cross-platform stability. |
| | | 113 | | func parseSlnMembers(path string, cfg options) []string { |
| | | 114 | | data := readManifestOrNil(path, cfg) |
| | | 115 | | if data == nil { |
| | | 116 | | return nil |
| | | 117 | | } |
| | | 118 | | var members []string |
| | | 119 | | seen := make(map[string]bool) |
| | | 120 | | for line := range strings.SplitSeq(string(data), "\n") { |
| | | 121 | | trim := strings.TrimSpace(line) |
| | | 122 | | if !strings.HasPrefix(trim, "Project(") { |
| | | 123 | | continue |
| | | 124 | | } |
| | | 125 | | m := slnProjectLineRE.FindStringSubmatch(trim) |
| | | 126 | | if len(m) != 2 { |
| | | 127 | | continue |
| | | 128 | | } |
| | | 129 | | p := strings.ReplaceAll(m[1], `\`, "/") |
| | | 130 | | ext := strings.ToLower(filepath.Ext(p)) |
| | | 131 | | if ext != ".csproj" && ext != ".fsproj" && ext != ".vbproj" { |
| | | 132 | | continue |
| | | 133 | | } |
| | | 134 | | dir := filepath.ToSlash(filepath.Dir(p)) |
| | | 135 | | if dir == "" || dir == "." { |
| | | 136 | | continue |
| | | 137 | | } |
| | | 138 | | if !seen[dir] { |
| | | 139 | | seen[dir] = true |
| | | 140 | | members = append(members, dir) |
| | | 141 | | } |
| | | 142 | | } |
| | | 143 | | return members |
| | | 144 | | } |