mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
f9530d7d5f
C# / Java / Kotlin receivers that are fields, properties or parameters were typed only when declared in the calling method without generics, so a call through a field fell to the receiver-word guess: Newtonsoft's _innerWriter.WriteValue() landed on the wrapper's own WriteValue, a TextWriter's Write() on a test writer's, commons-lang's DateFormat field's parse() on DateParser 284 times. - read class-level member declarations (brace depth 1, comments skipped), walking base classes with their type arguments substituted, C# using aliases, type-parameter bounds, generic and for-each declarations - C# interface members default public and namespace-level types internal (TS extractor + kernel, parity 0 diffs); interface calls resolve at all - the receiver-word guess weighs the owner type's name only, the receiver's last link (a constant's type), and its head noun; .NET std names need a receiver named after the owner; production code never lands in a test suite's mock Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
5.4 KiB
TypeScript
181 lines
5.4 KiB
TypeScript
/**
|
||
* A C# / Java call through a field, property or parameter is a call on the
|
||
* type it is declared with — read from the class's own member declarations,
|
||
* those it inherits (with the type arguments the subclass gives), a using
|
||
* alias, or a generic or enhanced-for declaration — never a guess at a
|
||
* same-named method of some project class:
|
||
*
|
||
* - Newtonsoft's `_innerWriter.WriteValue(…)` inside TraceJsonWriter went to
|
||
* TraceJsonWriter's own `WriteValue`, and `_textWriter.Write(…)` on a
|
||
* `TextWriter` to a test's `ThrowingWriter.Write`;
|
||
* - a C# interface's members are public, so a call on an interface-typed
|
||
* field reaches the interface's method (eShop's `_fixUriService.Fix…()`);
|
||
* - `using Assert = …XUnitAssert;` makes `Assert.AreEqual` XUnitAssert's;
|
||
* - a standard .NET name on an untyped receiver (`table.Columns.Add(…)`) is
|
||
* not a project class's same-named method unless the receiver names it.
|
||
*/
|
||
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-member-receiver-'));
|
||
const files: Record<string, string> = {
|
||
'src/Json/JsonWriter.cs': `namespace App.Json;
|
||
|
||
public abstract class JsonWriter
|
||
{
|
||
public virtual void WriteValue(string value) { }
|
||
}
|
||
`,
|
||
'src/Json/TraceJsonWriter.cs': `using System.IO;
|
||
|
||
namespace App.Json;
|
||
|
||
internal class TraceJsonWriter : JsonWriter
|
||
{
|
||
private readonly JsonWriter _innerWriter;
|
||
/* a block comment naming the _textWriter field */
|
||
private readonly TextWriter _textWriter;
|
||
|
||
public override void WriteValue(string value)
|
||
{
|
||
_innerWriter.WriteValue(value);
|
||
_textWriter.Write(value);
|
||
}
|
||
}
|
||
`,
|
||
'tests/ThrowingWriter.cs': `using System.IO;
|
||
|
||
namespace App.Tests;
|
||
|
||
public class ThrowingWriter : TextWriter
|
||
{
|
||
public override void Write(string value) { }
|
||
}
|
||
`,
|
||
'tests/XUnitAssert.cs': `namespace App.Tests;
|
||
|
||
public static class XUnitAssert
|
||
{
|
||
public static void AreEqual(object a, object b) { }
|
||
}
|
||
`,
|
||
'src/DefaultJsonNameTable.cs': `namespace App;
|
||
|
||
public class DefaultJsonNameTable
|
||
{
|
||
public void Add(string key) { }
|
||
}
|
||
`,
|
||
'src/Services/IFixUriService.cs': `namespace App.Services;
|
||
|
||
public interface IFixUriService
|
||
{
|
||
void FixBasketItemPictureUri(string uri);
|
||
}
|
||
`,
|
||
'src/Services/FixUriService.cs': `namespace App.Services;
|
||
|
||
public class FixUriService : IFixUriService
|
||
{
|
||
public void FixBasketItemPictureUri(string uri) { }
|
||
}
|
||
`,
|
||
'src/Services/BasketService.cs': `namespace App.Services;
|
||
|
||
public class BasketService
|
||
{
|
||
private readonly IFixUriService _fixUriService;
|
||
|
||
public void Load(string uri)
|
||
{
|
||
_fixUriService.FixBasketItemPictureUri(uri);
|
||
}
|
||
}
|
||
`,
|
||
'tests/Integration/IntegrationTest.cs': `namespace App.Tests.Integration;
|
||
|
||
public abstract class IntegrationTest<TFixture> where TFixture : new()
|
||
{
|
||
protected TFixture Fixture { get; private set; }
|
||
}
|
||
|
||
public class DropCreateDatabaseAlways
|
||
{
|
||
public object CreateContext() => null;
|
||
}
|
||
`,
|
||
'tests/Integration/QueryTests.cs': `using Assert = App.Tests.XUnitAssert;
|
||
|
||
namespace App.Tests.Integration;
|
||
|
||
public class QueryTests : IntegrationTest<QueryTests.DatabaseInitializer>
|
||
{
|
||
public class DatabaseInitializer : DropCreateDatabaseAlways { }
|
||
|
||
public void Runs()
|
||
{
|
||
var context = Fixture.CreateContext();
|
||
Assert.AreEqual(1, 1);
|
||
var table = MakeTable();
|
||
table.Columns.Add("price");
|
||
}
|
||
}
|
||
`,
|
||
};
|
||
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 });
|
||
});
|
||
|
||
/** `Owner::member` of every call edge out of a file. */
|
||
function callsFrom(file: string): string[] {
|
||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||
return cg
|
||
.getOutgoingEdgesFrom(ids)
|
||
.filter((e) => e.kind === 'calls')
|
||
.map((e) => cg.getNode(e.target)!.qualifiedName)
|
||
.sort();
|
||
}
|
||
|
||
describe('C# calls through declared members', () => {
|
||
it('reach the field’s declared type, and nothing for an outside one', () => {
|
||
expect(callsFrom('src/Json/TraceJsonWriter.cs')).toEqual(['App.Json::JsonWriter::WriteValue']);
|
||
});
|
||
|
||
it('reach an interface’s method: interface members are public', () => {
|
||
expect(callsFrom('src/Services/BasketService.cs')).toEqual([
|
||
'App.Services::IFixUriService::FixBasketItemPictureUri',
|
||
]);
|
||
});
|
||
|
||
it('follow an inherited property typed by the type argument, a using alias, and leave a .NET name alone', () => {
|
||
expect(callsFrom('tests/Integration/QueryTests.cs')).toEqual([
|
||
'App.Tests.Integration::DropCreateDatabaseAlways::CreateContext',
|
||
'App.Tests::XUnitAssert::AreEqual',
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe('C# default visibility', () => {
|
||
it('is public in an interface and internal for a type in a namespace', () => {
|
||
const method = cg.getNodesInFile('src/Services/IFixUriService.cs').find((n) => n.kind === 'method')!;
|
||
expect(method.visibility).toBe('public');
|
||
const cls = cg.getNodesInFile('src/Json/TraceJsonWriter.cs').find((n) => n.kind === 'class')!;
|
||
expect(cls.visibility).toBe('internal');
|
||
});
|
||
});
|