fix(csharp): namespaces, usings and nested types follow the language's scopes (#2220)

A block namespace qualifies only its own body (a nested one is Outer.Inner),
mirrored in the kernel; a using names the project's namespace, never another
file's using; a global using is its own project's; a nested type is reached
by bare name only from inside its owner or a type deriving from it, through
any partial part; visible C# types are filtered before ranking.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 23:56:34 +00:00
committed by GitHub
parent 03123335b0
commit ff75a6ca5f
7 changed files with 391 additions and 51 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- C# names now follow the language's scopes. A block `namespace X { … }` qualifies only the types inside it, so a type declared after the block, or in a file's second namespace, is no longer filed under the first. A namespace written inside another is `Outer.Inner`. A `using` links to the project's namespace instead of another file's `using` of the same name. A `global using` applies only within its own project. A nested type is reachable by bare name only from inside its owner or a type deriving from it, including through another partial part. Before, serilog's `Guard` was filed under `JetBrains.Annotations`, its tests' `Some.InformationEvent()` went to the performance tests' `Some`, and AutoMapper's same-file `new Source()` could reach another test class's nested `Source`.
- A function or name passed as a value now resolves to what is in scope where it's written. A function nested inside another function is only reachable from inside it. In Python, a parameter or local of the same name is that local. A pytest fixture is a test's parameter only in its own module or under its `conftest.py`. Before, httpx's `self._build_auth(auth)` linked to an `auth` a test defines inside another function, and `auth_flow(self, request)` handing `request` on linked to the package's `request()` function.
- In Python, an attribute passed as a value through an object nothing types, like netbox's `device=self.parent.device`, is no longer linked to the project's only method of that name when the object isn't named after the method's class. Before, netbox's model-field reads went to a GraphQL filter's `device` method, and healthchecks' `check.last_ping` went to a notification transport's.
- In Scala, a kind-projector placeholder in a type, like `Align[Either[A, *]]`, is no longer linked to an operator method named `*`. A symbolic name in a type, like cats' `F ~> G`, now resolves only to a type.
+172
View File
@@ -0,0 +1,172 @@
/**
* C# names follow the scopes the language gives them:
* - a block `namespace X { … }` holds only its body — serilog's Guard.cs
* declares `static class Guard` after a `namespace JetBrains.Annotations`
* block, and a file's second namespace (or one nested inside another,
* `Outer.Inner`) is its own;
* - `using X;` names the project's namespace X, never another file's using;
* - a `global using` is its own project's — serilog's two test projects each
* `global using` their own `Support` namespace, and both define `Some`;
* - a nested type is reached by bare name only from inside its owner or a
* type deriving from it, through any partial part.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-csharp-ns-'));
const files: Record<string, string> = {
'src/Lib/Lib.csproj': '<Project Sdk="Microsoft.NET.Sdk"></Project>\n',
'src/Lib/Guard.cs': `using JetBrains.Annotations;
namespace JetBrains.Annotations
{
sealed class NoEnumerationAttribute : System.Attribute { }
}
static class Guard
{
public static T AgainstNull<T>(T value) where T : class => value;
}
`,
'src/Lib/Multi.cs': `namespace Alpha
{
class X { }
}
namespace Beta
{
class Y { }
namespace Inner
{
class Z { }
}
}
`,
'src/Lib/Uses.cs': `using Beta;
namespace Gamma
{
class Consumer { }
}
`,
'src/Lib/Reader.cs': `namespace Lib
{
public abstract class Reader
{
internal enum State { Start, Done }
}
public partial class TextReader : Reader
{
}
}
`,
'src/Lib/TextReader.Async.cs': `namespace Lib
{
public partial class TextReader
{
object Peek() => State.Start;
}
}
`,
'src/Lib/Writer.cs': `namespace Lib
{
public abstract class Writer
{
internal enum State { Start, Done }
object Current() => State.Done;
}
}
`,
'test/Unit/Unit.csproj': '<Project Sdk="Microsoft.NET.Sdk"></Project>\n',
'test/Unit/GlobalUsings.cs': 'global using Unit.Support;\n',
'test/Unit/Support/Some.cs': `namespace Unit.Support;
static class Some
{
public static int InformationEvent() => 1;
}
`,
'test/Unit/LogTests.cs': `namespace Unit.Context;
class LogTests
{
int Run() => Some.InformationEvent();
}
`,
'test/Unit/MappingTests.cs': `namespace Unit.Mapping;
class FirstCase
{
class Source { }
object Make() => new Source();
}
class SecondCase
{
class Source { }
object Make() => new Source();
}
`,
'test/Perf/Perf.csproj': '<Project Sdk="Microsoft.NET.Sdk"></Project>\n',
'test/Perf/GlobalUsings.cs': 'global using Perf.Support;\n',
'test/Perf/Support/Some.cs': `namespace Perf.Support;
static class Some
{
public static int InformationEvent() => 2;
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const qualifiedNames = (file: string) => cg.getNodesInFile(file).filter((n) => n.kind !== 'file').map((n) => `${n.kind} ${n.qualifiedName}`);
const targetsAt = (file: string, line: number, kind?: string) => {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.line === line && (!kind || e.kind === kind))
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.filePath}:${t.qualifiedName}`);
};
describe('C# namespaces, usings and nested types', () => {
it('a block namespace qualifies only its own body', () => {
expect(qualifiedNames('src/Lib/Guard.cs')).toEqual(expect.arrayContaining([
'class JetBrains.Annotations::NoEnumerationAttribute', 'class Guard', 'method Guard::AgainstNull',
]));
expect(qualifiedNames('src/Lib/Multi.cs')).toEqual(expect.arrayContaining([
'class Alpha::X', 'class Beta::Y', 'class Beta.Inner::Z',
]));
});
it('a using names the project’s namespace', () => {
expect(targetsAt('src/Lib/Uses.cs', 1, 'imports')).toEqual(['namespace src/Lib/Multi.cs:Beta']);
});
it('a global using is its own project’s', () => {
expect(targetsAt('test/Unit/LogTests.cs', 5, 'calls')).toEqual(['method test/Unit/Support/Some.cs:Unit.Support::Some::InformationEvent']);
});
it('a nested type is its owner’s, or a derived type’s through any partial part', () => {
expect(targetsAt('test/Unit/MappingTests.cs', 12, 'instantiates')).toEqual(['class test/Unit/MappingTests.cs:Unit.Mapping::SecondCase::Source']);
expect(targetsAt('src/Lib/TextReader.Async.cs', 5)).toContain('enum src/Lib/Reader.cs:Lib::Reader::State');
expect(targetsAt('src/Lib/Writer.cs', 7)).toContain('enum src/Lib/Writer.cs:Lib::Writer::State');
});
});
@@ -179,3 +179,8 @@ namespace Torture.Beta
{
public class Other { }
}
static class TopLevelGuard
{
public static T AgainstNull<T>(T value) where T : class => value;
}
+53 -22
View File
@@ -205,34 +205,21 @@ pub fn extract(file_path: &str, source: &str) -> Result<EmitOut, String> {
w.node_ids.push(ids::file_node_id(file_path));
w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
// extractFilePackage: the FIRST top-level namespace declaration mints ONE
// `namespace` node that stays pushed for the ENTIRE file — a second
// top-level namespace's types nest under the first's node/QN, nested
// namespaces leave no trace, and every import ref in a namespaced file
// hangs off this node (checklist §namespace).
// extractFilePackage: a top-level file-scoped `namespace X;` mints ONE
// `namespace` node that stays pushed for the ENTIRE file, so every import
// ref in the file hangs off it (checklist §namespace). A block namespace
// scopes only its own body — visit_node.
let root = tree.root_node();
let mut pkg_pushed = false;
for i in 0..root.named_child_count() {
let Some(child) = root.named_child(i) else { continue };
if child.kind() != "namespace_declaration"
&& child.kind() != "file_scoped_namespace_declaration"
{
if child.kind() != "file_scoped_namespace_declaration" {
continue;
}
// csharpExtractor.extractPackage: `name` field ?? first
// qualified_name/identifier named child. No trim.
let name_node = child.child_by_field_name("name").or_else(|| {
(0..child.named_child_count())
.filter_map(|j| child.named_child(j))
.find(|c| matches!(c.kind(), "qualified_name" | "identifier"))
});
if let Some(name_node) = name_node {
let pkg = w.text(name_node).to_string();
if !pkg.is_empty() {
if let Some(row) = w.create_node("namespace", &pkg, child, Extra::default()) {
w.stack.push(Scope { row, kind: "namespace", name: pkg });
pkg_pushed = true;
}
if let Some(pkg) = namespace_name(&w, child) {
if let Some(row) = w.create_node("namespace", &pkg, child, Extra::default()) {
w.stack.push(Scope { row, kind: "namespace", name: pkg });
pkg_pushed = true;
}
}
break;
@@ -265,6 +252,18 @@ fn record_is_struct(node: Node) -> bool {
.any(|c| c.kind() == "struct")
}
/// csharpExtractor.extractPackage: `name` field ?? first qualified_name /
/// identifier named child. No trim; None when empty.
fn namespace_name(w: &Walker, node: Node) -> Option<String> {
let name_node = node.child_by_field_name("name").or_else(|| {
(0..node.named_child_count())
.filter_map(|j| node.named_child(j))
.find(|c| matches!(c.kind(), "qualified_name" | "identifier"))
})?;
let name = w.text(name_node);
(!name.is_empty()).then(|| name.to_string())
}
impl<'t> Walker<'t> {
fn text(&self, node: Node) -> &'t str {
&self.src[node.byte_range()]
@@ -518,6 +517,38 @@ impl<'t> Walker<'t> {
let kind = node.kind();
let mut skip_children = false;
// A block namespace scopes only its body (tree-sitter.ts visitNode):
// one written inside another is `Outer.Inner`, taking the outer's
// place on the stack while its body is walked.
if kind == "namespace_declaration" {
if let Some(ns_name) = namespace_name(self, node) {
let outer = match self.stack.last() {
Some(top) if top.kind == "namespace" => self.stack.pop(),
_ => None,
};
let full = match &outer {
Some(o) => format!("{}.{}", o.name, ns_name),
None => ns_name,
};
let row = self.create_node("namespace", &full, node, Extra::default());
if let Some(row) = row {
self.stack.push(Scope { row, kind: "namespace", name: full });
}
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.visit_node(c);
}
}
if row.is_some() {
self.stack.pop();
}
if let Some(o) = outer {
self.stack.push(o);
}
return;
}
}
self.maybe_capture_fn_refs(node);
if kind == "class_declaration" || kind == "record_declaration" {
+4 -4
View File
@@ -82,10 +82,10 @@ export const csharpExtractor: LanguageExtractor = {
typeAliasTypes: [],
// Namespaces qualify type names so same-named types in different namespaces are
// distinguishable (e.g. `ApplicationCore.Entities.CatalogBrand` vs
// `BlazorShared.Models.CatalogBrand`). Both block (`namespace Foo { … }`, which
// nests its types) and file-scoped (`namespace Foo;`) forms — extractFilePackage
// pushes the namespace onto the scope so nested/top-level types pick it up.
packageTypes: ['namespace_declaration', 'file_scoped_namespace_declaration'],
// `BlazorShared.Models.CatalogBrand`). A file-scoped `namespace Foo;` covers the
// whole file (extractFilePackage); a block `namespace Foo { … }` only its body
// (the visitor scopes it).
packageTypes: ['file_scoped_namespace_declaration'],
extractPackage: (node: SyntaxNode, source: string) => {
const name =
node.childForFieldName('name') ??
+26
View File
@@ -1140,6 +1140,32 @@ export class TreeSitterExtractor {
}
}
// C# block namespaces scope only their own body: serilog's Guard.cs opens
// `namespace JetBrains.Annotations { … }` and then declares `static class
// Guard` at the top level, and a file's second namespace is its own. A
// namespace written inside another is `Outer.Inner` — the dotted name a
// type's qualifiedName leads with — so it takes the outer's place on the
// scope while its body is walked. (A file-scoped `namespace X;` covers
// the whole file: extractFilePackage.) Mirrored in the kernel (csharp.rs).
if (this.language === 'csharp' && nodeType === 'namespace_declaration') {
const nsName = this.extractor.extractPackage?.(node, this.source);
if (nsName) {
const topId = this.nodeStack[this.nodeStack.length - 1];
const top = this.nodes.find((n) => n.id === topId);
const outer = top?.kind === 'namespace' ? top : null;
if (outer) this.nodeStack.pop();
const ns = this.createNode('namespace', outer ? `${outer.name}.${nsName}` : nsName, node);
if (ns) this.nodeStack.push(ns.id);
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) this.visitNode(child);
}
if (ns) this.nodeStack.pop();
if (outer) this.nodeStack.push(outer.id);
return;
}
}
// Function-as-value capture (#756) — independent of the dispatch ladder
// below (the captured container types have no other handler there), so it
// can never shadow or be shadowed by an extraction branch.
+130 -25
View File
@@ -1339,7 +1339,8 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex
// not a test entity `Record`, tokio's `Output` not a `runtime/tests` type.
if (isTestSuitePath(candidate.filePath) && !isTestPath(ref.filePath)) return false;
if (candidate.language === 'csharp' && ref.language === 'csharp' && CSHARP_TYPE_KINDS.has(candidate.kind) &&
/^[A-Za-z_]\w*$/.test(ref.referenceName) && !isCsharpTypeVisible(candidate, ref, context)) return false;
/^[A-Za-z_]\w*$/.test(ref.referenceName) &&
(!isCsharpTypeVisible(candidate, ref, context) || !isCsharpNestedTypeInScope(candidate, ref, context))) return false;
const lang = candidate.language as string;
if (lang === 'c' || lang === 'cpp') {
return (
@@ -3265,12 +3266,13 @@ function csharpProjectStaticUsings(dir: string, context: ResolutionContext, memo
}
const CSHARP_NAMESPACE_SCOPES = new WeakMap<ResolutionContext, Map<string, { namespaces: string[]; usings: Set<string>; aliases: Map<string, string> }>>();
const CSHARP_PROJECT_USINGS = new WeakMap<ResolutionContext, Map<string, Set<string>>>();
const CSHARP_PROJECT_USINGS = new WeakMap<ResolutionContext, Map<string, { usings: Set<string>; project: boolean }>>();
/**
* The namespaces a C# file's code runs in and the ones it imports: its
* `namespace` declarations, its `using X;`, every file's `global using X;`
* and the `<Using Include="X" />` of the project files above it.
* `namespace` declarations, its `using X;`, the `global using X;` of its
* project's files (every file's, outside any project) and the
* `<Using Include="X" />` of the project files above it.
*/
function csharpNamespaceScope(file: string, context: ResolutionContext): { namespaces: string[]; usings: Set<string>; aliases: Map<string, string> } {
let memo = CSHARP_NAMESPACE_SCOPES.get(context);
@@ -3281,7 +3283,9 @@ function csharpNamespaceScope(file: string, context: ResolutionContext): { names
const namespaces = [...text.matchAll(/^\s*namespace\s+([\w.]+)/gm)].map((m) => m[1]!);
let projectMemo = CSHARP_PROJECT_USINGS.get(context);
if (!projectMemo) CSHARP_PROJECT_USINGS.set(context, (projectMemo = new Map()));
const usings = new Set<string>(csharpProjectUsings(path.posix.dirname(file), context, projectMemo));
const project = csharpProjectUsings(path.posix.dirname(file), context, projectMemo);
const usings = new Set<string>(project.usings);
if (!project.project) for (const u of csharpGlobalUsings('.', context)) usings.add(u);
const aliases = new Map<string, string>();
for (const m of text.matchAll(/^\s*(?:global\s+)?using\s+(?!static\b)(?:([A-Za-z_]\w*)\s*=\s*)?([\w.]+)\s*;/gm)) {
if (m[1]) aliases.set(m[1], m[2]!);
@@ -3292,42 +3296,65 @@ function csharpNamespaceScope(file: string, context: ResolutionContext): { names
return scope;
}
/** `global using X;` from any file, and `<Using Include="X" />` of the project files from `dir` up. */
function csharpProjectUsings(dir: string, context: ResolutionContext, memo: Map<string, Set<string>>): Set<string> {
/**
* The `<Using Include="X" />` of the project files from `dir` up, and the
* `global using X;` of each project's own files — a global using is its
* project's alone: serilog's Serilog.Tests and Serilog.PerformanceTests each
* `global using` their own `Support` namespace, and both define `Some`.
* `project` says whether a `.csproj` sits at `dir` or above it.
*/
function csharpProjectUsings(dir: string, context: ResolutionContext, memo: Map<string, { usings: Set<string>; project: boolean }>): { usings: Set<string>; project: boolean } {
const key = dir;
const hit = memo.get(key);
if (hit) return hit;
let usings: Set<string>;
if (dir === '.' || dir === '' || dir === '/') {
usings = new Set();
for (const f of context.getAllFiles()) {
if (!f.endsWith('.cs') || (context.fileContains && !context.fileContains(f, 'global using'))) continue;
for (const m of (context.readFile(f) ?? '').matchAll(/^\s*global\s+using\s+(?!static\b)([\w.]+)\s*;/gm)) usings.add(m[1]!);
}
} else {
usings = new Set(csharpProjectUsings(path.posix.dirname(dir), context, memo));
}
const root = dir === '.' || dir === '' || dir === '/';
const parent = root ? null : csharpProjectUsings(path.posix.dirname(dir), context, memo);
const usings = new Set<string>(parent?.usings ?? []);
let project = parent?.project ?? false;
let entries: string[] = [];
try {
entries = fs.readdirSync(path.join(context.getProjectRoot(), dir === '.' ? '' : dir));
entries = fs.readdirSync(path.join(context.getProjectRoot(), root ? '' : dir));
} catch {
entries = [];
}
for (const entry of entries) {
if (!/\.(?:csproj|props)$/i.test(entry)) continue;
let project = '';
let text = '';
try {
project = fs.readFileSync(path.join(context.getProjectRoot(), dir === '.' ? '' : dir, entry), 'utf8');
text = fs.readFileSync(path.join(context.getProjectRoot(), root ? '' : dir, entry), 'utf8');
} catch {
continue;
}
for (const m of project.matchAll(/<Using\s+Include\s*=\s*"([\w.]+)"(?![^>]*\bStatic\s*=\s*"true")[^>]*>/gi)) usings.add(m[1]!);
if (/\.csproj$/i.test(entry) && !project) {
project = true;
for (const u of csharpGlobalUsings(root ? '.' : dir, context)) usings.add(u);
}
for (const m of text.matchAll(/<Using\s+Include\s*=\s*"([\w.]+)"(?![^>]*\bStatic\s*=\s*"true")[^>]*>/gi)) usings.add(m[1]!);
// The SDK's implicit usings (serilog's own `System.TimeProvider` polyfill is seen through them).
if (/<ImplicitUsings>\s*(?:enable|true)\s*<\/ImplicitUsings>/i.test(project)) {
if (/<ImplicitUsings>\s*(?:enable|true)\s*<\/ImplicitUsings>/i.test(text)) {
for (const ns of CSHARP_IMPLICIT_USINGS) usings.add(ns);
}
}
memo.set(key, usings);
const result = { usings, project };
memo.set(key, result);
return result;
}
const CSHARP_GLOBAL_USINGS = new WeakMap<ResolutionContext, Map<string, Set<string>>>();
/** The `global using X;` of the `.cs` files under `dir` (`.` = the whole repository). */
function csharpGlobalUsings(dir: string, context: ResolutionContext): Set<string> {
let memo = CSHARP_GLOBAL_USINGS.get(context);
if (!memo) CSHARP_GLOBAL_USINGS.set(context, (memo = new Map()));
const hit = memo.get(dir);
if (hit) return hit;
const usings = new Set<string>();
const prefix = dir === '.' ? '' : `${dir}/`;
for (const f of context.getAllFiles()) {
if (!f.endsWith('.cs') || !f.startsWith(prefix) || (context.fileContains && !context.fileContains(f, 'global using'))) continue;
for (const m of (context.readFile(f) ?? '').matchAll(/^\s*global\s+using\s+(?!static\b)([\w.]+)\s*;/gm)) usings.add(m[1]!);
}
memo.set(dir, usings);
return usings;
}
@@ -3356,6 +3383,60 @@ function isCsharpTypeVisible(n: Node, ref: UnresolvedRef, context: ResolutionCon
return scope.usings.has(ns);
}
/**
* Whether a bare C# name can reach `n` as a nested type: only from inside the
* type that declares it (any partial part, any depth) or a class deriving from
* it. AutoMapper's tests each declare their own nested `Source`, and a
* same-file `new Source()` went to whichever test class came first.
*/
function isCsharpNestedTypeInScope(n: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
const cut = n.qualifiedName.lastIndexOf('::');
if (cut < 0) return true;
const owner = n.qualifiedName.slice(0, cut);
const ownerType = context.getNodesInFile(n.filePath).find((p) => CSHARP_TYPE_KINDS.has(p.kind) && p.qualifiedName === owner);
// Declared in a namespace, not a type.
if (!ownerType) return true;
const enclosing = context.getNodesInFile(ref.filePath)
.filter((p) => CSHARP_TYPE_KINDS.has(p.kind) && p.startLine <= ref.line && p.endLine >= ref.line);
if (enclosing.some((p) => p.qualifiedName === owner || p.qualifiedName.startsWith(`${owner}::`))) return true;
// Inherited: a type around the ref derives from the owner (`class SourceA :
// Source`), through any partial part — Newtonsoft's JsonTextReader.Async.cs
// is `partial class JsonTextReader` with no base list, reading JsonReader's `State`.
return enclosing.some((p) => csharpAncestorNames(p.qualifiedName, context).has(ownerType.name));
}
const CSHARP_ANCESTORS = new WeakMap<ResolutionContext, Map<string, Set<string>>>();
/** The simple names of the C# types `qn` derives from, through every partial part and base, a few levels up. */
function csharpAncestorNames(qn: string, context: ResolutionContext, depth = 0): Set<string> {
let memo = CSHARP_ANCESTORS.get(context);
if (!memo) CSHARP_ANCESTORS.set(context, (memo = new Map()));
const hit = memo.get(qn);
if (hit) return hit;
const names = new Set<string>();
memo.set(qn, names); // a cycle reads what is gathered so far
for (const decl of context.getNodesByQualifiedName(qn)) {
if (decl.language !== 'csharp' || !CSHARP_TYPE_KINDS.has(decl.kind)) continue;
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? [];
let header = '';
for (let i = decl.startLine - 1; i < Math.min(lines.length, decl.startLine + 6) && !header.includes('{'); i++) header += `${lines[i] ?? ''} `;
const list = /:\s*([^{;]*)/.exec(header.split('{')[0]!.replace(/\bwhere\b[\s\S]*$/, ''))?.[1] ?? '';
for (const base of splitCppTopLevel(list)) {
const name = /([A-Za-z_]\w*)\s*(?:<.*)?$/.exec(base.trim())?.[1];
if (name) names.add(name);
}
}
if (depth < 4) {
for (const base of [...names]) {
for (const t of context.getNodesByName(base)) {
if (t.language !== 'csharp' || !CSHARP_TYPE_KINDS.has(t.kind) || t.qualifiedName === qn) continue;
for (const up of csharpAncestorNames(t.qualifiedName, context, depth + 1)) names.add(up);
}
}
}
return names;
}
const OBJC_SUPERS = new WeakMap<ResolutionContext, Map<string, string[]>>();
const OBJC_MEMBER_KINDS: ReadonlySet<string> = new Set(['method', 'property', 'field']);
@@ -4395,6 +4476,13 @@ export function matchByExactName(
(!importRef || isImportableKind(n.kind)) &&
// Nested locals are only reachable from inside their container (#1230).
isLexicallyReachable(n, ref, context) &&
// A C# type name is a type its namespaces can see — ahead of the ranking,
// so a visible namesake wins where the veto after it would drop the
// ref: eShop's `WebhookType.OrderPaid` under `using Webhooks.API.Model;`.
// A nested type, only from inside its owner: AutoMapper's same-file `new
// Source()` in one test class is not the previous test class's `Source`.
!(ref.language === 'csharp' && n.language === 'csharp' && CSHARP_TYPE_KINDS.has(n.kind) && /^[A-Za-z_]\w*$/.test(ref.referenceName) &&
(!isCsharpNestedTypeInScope(n, ref, context) || (n.filePath !== ref.filePath && !isCsharpTypeVisible(n, ref, context)))) &&
// Preserve import ranking; calls reject the winner without promoting another.
(!importRef || n.filePath === ref.filePath ||
!ESM_FAMILY.has(n.language) || !isSealedModule(n.filePath, context)) &&
@@ -4474,7 +4562,14 @@ export function matchByQualifiedName(
)
: nodes;
const candidates = keepForRef(context.getNodesByQualifiedName(ref.referenceName));
let candidates = keepForRef(context.getNodesByQualifiedName(ref.referenceName));
// A C# `using X.Y;` names a namespace: one the project declares, else it is
// the file's own (external) using — never another file's using of that name.
if (ref.language === 'csharp' && ref.referenceKind === 'imports') {
const namespaces = candidates.filter((n) => n.kind === 'namespace');
candidates = namespaces.length > 0 ? preferCallSiteFile(namespaces, ref.filePath).slice(0, 1)
: candidates.filter((n) => n.kind !== 'import' || n.filePath === ref.filePath);
}
if (candidates.length === 1) {
return {
@@ -5508,6 +5603,8 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
CSHARP_STATIC_USINGS.delete(context);
CSHARP_NAMESPACE_SCOPES.delete(context);
CSHARP_PROJECT_USINGS.delete(context);
CSHARP_GLOBAL_USINGS.delete(context);
CSHARP_ANCESTORS.delete(context);
SCALA_SUPERS.delete(context);
SCALA_IMPORTS.delete(context);
ESM_EXPORT_LISTS.delete(context);
@@ -6756,10 +6853,18 @@ export function matchMethodCall(
// own file first — otherwise the first-indexed class wins and a call in `b/`
// resolves to `a/`'s method (#1079).
const strat1 = nmTimedT('mc-class', ref, (): ResolvedRef | null => {
const classCandidates = preferCallSiteFile(
let classCandidates = preferCallSiteFile(
context.getNodesByName(objectOrClass!).filter(isMethodOwnerKind),
ref.filePath,
);
// A C# class the call's namespaces can see before one they can't:
// serilog's `Some.InformationEvent()` in Serilog.Tests is its own
// Support namespace's `Some`, not the performance tests'.
if (ref.language === 'csharp' && classCandidates.length > 1) {
const typeRef = { ...ref, referenceName: objectOrClass! };
const visible = classCandidates.filter((c) => c.language !== 'csharp' || isCsharpTypeVisible(c, typeRef, context));
classCandidates = [...visible, ...classCandidates.filter((c) => !visible.includes(c))];
}
for (const classNode of classCandidates) {
// Skip cross-language class matches