Fix C# --deps: add .csproj/packages.config parsing, fix namespace resolution, add directory-based import matching#47
Conversation
… matching Co-authored-by: JordanCoin <6958488+JordanCoin@users.noreply.github.com>
7ced3ea to
9978d98
Compare
C# namespaces include the project name as a prefix (e.g. MyApp.Models) but directories don't (just Models/). tryDirMatch now tries progressively shorter suffixes: MyApp/Models → Models, finding the actual directory. Verified with a realistic C# project: Program.cs importing Models, Services, Data all resolve correctly. Hub detection works (Models/User.cs and Models/Product.cs detected as hubs with 3 importers each). Added test: suffix_match_strips_namespace_prefix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0c3f3c5eaa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !isGoPkg && len(resolved) > 0 { | ||
| resolvedImports = append(resolvedImports, resolved...) |
There was a problem hiding this comment.
Keep ambiguous multi-file imports unresolved
BuildFileGraph now records every non-Go multi-match (resolvedImports = append(resolvedImports, resolved...)), so ambiguous imports become multiple dependencies instead of remaining unresolved. In projects with duplicate module/file names (for example import config with both app/config.py and tests/config.py), this will add false edges and inflate hub/dependency counts even though the runtime import resolves to a single module by search path. The previous len(resolved)==1 guard prevented this class of corruption.
Useful? React with 👍 / 👎.
| for i := 1; i < len(parts); i++ { | ||
| suffix := filepath.Join(parts[i:]...) | ||
| if files, ok := idx.byDir[suffix]; ok { |
There was a problem hiding this comment.
Constrain directory suffix fallback for namespace matching
tryDirMatch strips leading namespace segments until any trailing directory matches, which can map external namespaces to unrelated local folders (for example using System.Threading.Tasks; will match a local Tasks/ directory). This introduces false internal dependencies whenever a leaf directory name collides with framework/package namespaces. The suffix fallback should be constrained (e.g., only one known root-namespace trim) instead of matching arbitrary tails.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR improves C# support in codemap --deps by (1) extracting NuGet external dependencies from C# project manifests, (2) correcting C# using namespace extraction, and (3) enhancing internal dependency resolution by mapping C# namespaces to directories.
Changes:
- Add NuGet dependency extraction via
.csprojandpackages.configparsing inReadExternalDeps. - Fix C#
usingparsing to keep the full namespace (includingusing staticand alias forms). - Add directory-based dependency resolution (
tryDirMatch) and relax single-file resolution constraints for non-Go imports; add focused tests for these behaviors.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| scanner/deps.go | Add C# external dependency detection via .csproj and packages.config parsing. |
| scanner/deps_test.go | Add tests covering csproj/packages.config parsing and C# external deps discovery. |
| scanner/astgrep.go | Fix C# using extraction to return full namespaces (no last-segment stripping). |
| scanner/astgrep_test.go | Update C# import expectation to match new namespace extraction behavior. |
| scanner/filegraph.go | Allow multi-file resolutions for non-Go imports and add directory-based resolution strategy. |
| scanner/filegraph_test.go | Add tests for tryDirMatch and C# namespace directory resolution behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| t.Run("external namespace does not resolve", func(t *testing.T) { | ||
| got := fuzzyResolve("System.Collections.Generic", "MyApp/Program.cs", idx, "", nil, "") | ||
| if got != nil { |
There was a problem hiding this comment.
This test also uses a hard-coded slash-separated path ("MyApp/Program.cs"). Use filepath.FromSlash / filepath.Join to keep tests portable and consistent with other path-handling tests in this package.
| if err := os.WriteFile(filepath.Join(tmpDir, "MyApp.csproj"), []byte(csproj), 0644); err != nil { | ||
| t.Fatal(err) | ||
| } |
There was a problem hiding this comment.
File writes in this test use mode 0644, but other tests in this file use the Go 1.13+ 0o644 literal (e.g., earlier os.WriteFile(..., 0o644)). Consider using 0o644 here as well for consistency.
| if err := os.WriteFile(filepath.Join(tmpDir, "packages.config"), []byte(config), 0644); err != nil { | ||
| t.Fatal(err) | ||
| } |
There was a problem hiding this comment.
File writes in this test use mode 0644, while other tests in this file use 0o644. For consistency (and to match existing style in this file), consider switching this to 0o644.
| // tryDirMatch returns all files whose parent directory matches the given path. | ||
| // This resolves namespace-level imports (e.g. C# "using Foo.Bar;" -> "Foo/Bar/") | ||
| // where an import refers to a whole directory rather than a single file. | ||
| // It also tries progressively shorter suffixes to handle namespace prefixes | ||
| // (e.g. "MyApp/Models" tries "MyApp/Models" first, then "Models"). | ||
| func tryDirMatch(path string, idx *fileIndex) []string { | ||
| // Try exact match first | ||
| if files, ok := idx.byDir[path]; ok { | ||
| return files | ||
| } | ||
| // Normalize path separators for cross-platform compatibility | ||
| nativePath := filepath.FromSlash(path) | ||
| if nativePath != path { | ||
| if files, ok := idx.byDir[nativePath]; ok { | ||
| return files | ||
| } | ||
| } | ||
| // Try suffix match: strip leading segments progressively | ||
| // This handles namespace prefixes like MyApp.Models -> Models | ||
| parts := strings.Split(filepath.ToSlash(path), "/") | ||
| for i := 1; i < len(parts); i++ { | ||
| suffix := filepath.Join(parts[i:]...) | ||
| if files, ok := idx.byDir[suffix]; ok { | ||
| return files | ||
| } | ||
| } | ||
| return nil |
There was a problem hiding this comment.
tryDirMatch returns every file in a matching directory via idx.byDir, which currently includes all file types (e.g., .json, .csproj, README). This will cause directory-based namespace imports to create dependencies to non-source files and inflate counts/hubs. Consider filtering directory matches to recognized source extensions (e.g., via IsSourceExt / ResolverExtensions) or building idx.byDir only from source files for this strategy.
| // Exclude multi-file Go package imports to avoid inflating hub counts. | ||
| // Go package imports start with the module prefix and resolve to all | ||
| // files in that package. For all other imports (e.g., C# namespace | ||
| // imports that resolve via directory matching), allow multi-file | ||
| // resolution so inter-namespace dependencies are tracked. | ||
| isGoPkg := fg.Module != "" && strings.HasPrefix(imp, fg.Module) && len(resolved) > 1 | ||
| if !isGoPkg && len(resolved) > 0 { | ||
| resolvedImports = append(resolvedImports, resolved...) | ||
| } |
There was a problem hiding this comment.
When allowing multi-file resolutions, resolvedImports = append(resolvedImports, resolved...) can add the current file itself if the import resolves to its own directory (e.g., a C# file in MyApp/Models importing MyApp.Models). That creates self-dependencies and can skew importer counts/hub detection. Filter out a.Path from resolved before appending (and/or skip self-edges when building Importers).
| t.Run("namespace with multiple files resolves via directory", func(t *testing.T) { | ||
| got := fuzzyResolve("MyApp.Models", "MyApp/Services/UserService.cs", idx, "", nil, "") | ||
| sort.Strings(got) |
There was a problem hiding this comment.
These tests pass hard-coded slash-separated paths (e.g. "MyApp/Services/UserService.cs"). Elsewhere in this package tests use filepath.FromSlash / filepath.Join to stay portable on Windows; using those here would keep the test consistent and avoid path-separator edge cases.
| t.Run("namespace with single file resolves via directory", func(t *testing.T) { | ||
| got := fuzzyResolve("MyApp.Services", "MyApp/Program.cs", idx, "", nil, "") | ||
| want := []string{serviceCS} |
There was a problem hiding this comment.
These tests pass hard-coded slash-separated paths (e.g. "MyApp/Program.cs"). For cross-platform consistency with the rest of the scanner tests, prefer filepath.FromSlash / filepath.Join for paths in test inputs.
After v4.0.2's initial C# support,
codemap --depsstill showed0 depsand no external packages for all C# projects. Three root causes:Fixes
External deps (
scanner/deps.go).csprojandpackages.configwere never parsed, so NuGet packages never appeared in the header box.parseCsproj()extracting<PackageReference Include="...">elementsparsePackagesConfig()extracting<package id="...">elementsReadExternalDeps()now detects both (.csprojmatched by extension indefaultcase since filenames vary)Namespace extraction (
scanner/astgrep.go)extractImportPath()was stripping the last namespace component from C#usingdirectives — behavior copied from Java class imports, wrong for C# namespace imports:Also fixed for
using staticandusing Alias = ...forms.Directory-based resolution (
scanner/filegraph.go)C# namespaces map to directories, not single files. Added
tryDirMatch()as Strategy 6 infuzzyResolve(): after all existing strategies fail, look up the normalized namespace path (MyApp/Services) in thebyDirindex to match all.csfiles in that directory.Also relaxed the
len(resolved) == 1constraint inBuildFileGraphto allow multi-file matches — but only for non-Go imports. Go package imports (prefixed with the module path) remain single-match only to preserve hub detection accuracy.Original prompt
📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.