Skip to content

Fix C# --deps: add .csproj/packages.config parsing, fix namespace resolution, add directory-based import matching#47

Merged
JordanCoin merged 3 commits intomainfrom
copilot/fix-csharp-dependencies-issue
Mar 27, 2026
Merged

Fix C# --deps: add .csproj/packages.config parsing, fix namespace resolution, add directory-based import matching#47
JordanCoin merged 3 commits intomainfrom
copilot/fix-csharp-dependencies-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 15, 2026

After v4.0.2's initial C# support, codemap --deps still showed 0 deps and no external packages for all C# projects. Three root causes:

Fixes

External deps (scanner/deps.go)

.csproj and packages.config were never parsed, so NuGet packages never appeared in the header box.

  • Added parseCsproj() extracting <PackageReference Include="..."> elements
  • Added parsePackagesConfig() extracting <package id="..."> elements
  • ReadExternalDeps() now detects both (.csproj matched by extension in default case since filenames vary)

Namespace extraction (scanner/astgrep.go)

extractImportPath() was stripping the last namespace component from C# using directives — behavior copied from Java class imports, wrong for C# namespace imports:

// Before: using MyApp.Services; → "MyApp"   (unusable for resolution)
// After:  using MyApp.Services; → "MyApp.Services"  (correct)

Also fixed for using static and using Alias = ... forms.

Directory-based resolution (scanner/filegraph.go)

C# namespaces map to directories, not single files. Added tryDirMatch() as Strategy 6 in fuzzyResolve(): after all existing strategies fail, look up the normalized namespace path (MyApp/Services) in the byDir index to match all .cs files in that directory.

Also relaxed the len(resolved) == 1 constraint in BuildFileGraph to 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.

// using MyApp.Models; → normalizes to MyApp/Models
//   → tryDirMatch finds [MyApp/Models/User.cs, MyApp/Models/Product.cs]
//   → both counted as deps (not a Go pkg import, so multi-file allowed)
Original prompt

This section details on the original issue you should resolve

<issue_title>C# dependencies issues</issue_title>
<issue_description>Thanks for the fix! It recognizes the files and functions now but dependencies is always 0.

Originally posted by @gatusmart in #20 (comment)</issue_description>

Comments on the Issue (you are @copilot in this section)


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

Copilot AI changed the title [WIP] Fix C# dependencies issue to return correct counts Fix C# --deps: add .csproj/packages.config parsing, fix namespace resolution, add directory-based import matching Mar 15, 2026
Copilot AI requested a review from JordanCoin March 15, 2026 15:34
Copilot AI and others added 2 commits March 27, 2026 13:43
… matching

Co-authored-by: JordanCoin <6958488+JordanCoin@users.noreply.github.com>
@JordanCoin JordanCoin force-pushed the copilot/fix-csharp-dependencies-issue branch from 7ced3ea to 9978d98 Compare March 27, 2026 17:44
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>
@JordanCoin JordanCoin marked this pull request as ready for review March 27, 2026 17:51
Copilot AI review requested due to automatic review settings March 27, 2026 17:51
@JordanCoin JordanCoin merged commit 10dd2f7 into main Mar 27, 2026
12 checks passed
@JordanCoin JordanCoin deleted the copilot/fix-csharp-dependencies-issue branch March 27, 2026 17:51
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +81 to +82
if !isGoPkg && len(resolved) > 0 {
resolvedImports = append(resolvedImports, resolved...)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +311 to +313
for i := 1; i < len(parts); i++ {
suffix := filepath.Join(parts[i:]...)
if files, ok := idx.byDir[suffix]; ok {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .csproj and packages.config parsing in ReadExternalDeps.
  • Fix C# using parsing to keep the full namespace (including using static and 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.

Comment on lines +425 to +427
t.Run("external namespace does not resolve", func(t *testing.T) {
got := fuzzyResolve("System.Collections.Generic", "MyApp/Program.cs", idx, "", nil, "")
if got != nil {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +859 to +861
if err := os.WriteFile(filepath.Join(tmpDir, "MyApp.csproj"), []byte(csproj), 0644); err != nil {
t.Fatal(err)
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +885 to +887
if err := os.WriteFile(filepath.Join(tmpDir, "packages.config"), []byte(config), 0644); err != nil {
t.Fatal(err)
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +291 to +317
// 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
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to 83
// 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...)
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +408 to +410
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)
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +417 to +419
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}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C# dependencies issues

3 participants