mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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') ??
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user