Skip to content

C# import resolution never matches: resolveCSharpImport treats a namespace as a file path #645

Description

@SlyWombat

Version: plugin 2.9.4 · Skill: /understand · File: skills/understand/extract-import-map.mjs

Summary

resolveCSharpImport resolves a C# using by reusing the Java/Kotlin dotted-FQN strategy, which assumes the file-per-public-type naming convention. C# has no such rule — a using names a namespace, which can span many files and need not mirror the directory layout. The result is that C# import resolution silently produces nothing on any project whose folders do not exactly match its namespaces.

// extract-import-map.mjs:1266
export function resolveCSharpImport(rawImport, _file, ctx) {
  return resolveDottedFqn(rawImport, '.cs', ctx.csIndex);
}

// extract-import-map.mjs:1149
function resolveDottedFqn(fqn, ext, suffixIndex) {
  const filePart = trimmed.replace(/\./g, '/') + ext;   // "MegaPDF.Core.Engine" -> "MegaPDF/Core/Engine.cs"
  const matches = suffixIndex.get(filePart);
  return matches ? [...matches] : [];
}

using MegaPDF.Core.Engine; probes for a file at MegaPDF/Core/Engine.cs. The actual sources are src/MegaPDF.Core/Engine/IPdfEngine.cs, Geometry.cs, and so on — a directory, not a file. Nothing matches, so the import resolves to [].

Impact, measured on a real repo

A 180-file polyglot repo (52 C# files, ~67 internal using directives — MegaPDF.Core.Engine ×22, .Engine.Pdfium ×17, .Recovery ×11, .Editing ×10, .Services ×7) produced an import map with 2 edges across all 180 files, and neither was C#.

This is not merely missing edges. compute-batches.mjs clusters on the import graph, so with no edges it degraded to:

Info: compute-batches: merged 136 small batches (140 files) into 6 misc batches — singletons and orphans consolidated

Half the codebase then went to file-analyzer in arbitrary 25-file groups rather than semantic ones. Worse, the agent contract tells analyzers to use batchImportData and not re-resolve imports from source, so the honest outcome is a graph where the dominant language appears structurally uncoupled — 52 disconnected nodes. I only got a usable graph by explicitly overriding that instruction and having the analyzers map namespaces to directories by hand; all 141 imports edges in the final graph are hand-derived.

There is a related trap for downstream agents: file-analyzer's Step E self-validation checks emitted edges against batchImportData and neighborMap. When both are empty, its criteria are unsatisfiable — every correct hand-derived edge registers as a failure.

Repro

  1. Any C# project where namespaces do not mirror directories — e.g. a project folder src/Foo.Core/ declaring namespace Foo.Core.Engine in src/Foo.Core/Engine/*.cs.
  2. Run /understand.
  3. .ua/intermediate/scan-result.jsonimportMap has no entries for the C# files.

Suggested fix

Index C# files by declared namespace, not by path shape: parse namespace X.Y.Z; (file-scoped) and namespace X.Y.Z { … } (block) during extraction, build namespace → [files], and resolve using X.Y.Z to that set. Optionally narrow to files declaring types actually referenced in the consumer, which is what avoids fan-out to every file in a large namespace.

The same assumption may not hold for other namespace-based languages routed through resolveDottedFqn — worth a look at Scala, and at C++ if namespaces are ever added.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions