Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixes

- In VB.NET, reading or setting a `Shared` field or property through its class name, like `AppSession.SessionId` or `AppSession.CurrentUser = "demo"`, now counts as a use of that field or property and of the class, so `codegraph callers` and impact no longer miss it. (#2305)

## [1.6.2] - 2026-10-03

Expand Down
146 changes: 146 additions & 0 deletions __tests__/vbnet-shared-member-refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* VB.NET: reading or writing a Shared field/property through its class name
* (`AppSession.SessionId`, `AppSession.CurrentUser = "demo"`) links the caller
* to the member and to the class (#2305), without binding look-alike members.
*/
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;

function nodeByQn(qn: string) {
const found = allNodes().find((n) => n.qualifiedName === qn);
if (!found) throw new Error(`no node with qualifiedName ${qn}`);
return found;
}

function allNodes() {
return fs
.readdirSync(root)
.filter((f) => f.endsWith('.vb'))
.flatMap((f) => cg.getNodesInFile(f));
}

/** Qualified names of the symbols with a `references` edge into `qn`. */
function referencedBy(qn: string): string[] {
const id = nodeByQn(qn).id;
return cg
.getIncomingEdgesTo([id], ['references'])
.map((e) => cg.getNode(e.source)!.qualifiedName)
.sort();
}

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-shared-'));
const files: Record<string, string> = {
'AppSession.vb': `Public Class AppSession
Public Shared SessionId As Guid = Guid.NewGuid()
Public Shared Property CurrentUser As String
Public Shared Function GetGreeting() As String
Return "Hello " & CurrentUser
End Function
End Class
`,
'Consumer.vb': `Public Class Consumer
Public Sub Run()
Dim id As Guid = AppSession.SessionId
AppSession.CurrentUser = "demo"
Console.WriteLine(AppSession.GetGreeting())
End Sub
End Class
`,
'Consumer2.vb': `Public Class Consumer2
Public Function Describe() As String
Return AppSession.CurrentUser & AppSession.SessionId.ToString()
End Function
End Class
`,
'OtherSession.vb': `Public Class OtherSession
Public Shared SessionId As Guid = Guid.NewGuid()
Public Property Size As Integer
End Class
`,
'Size.vb': `Public Class Size
End Class
`,
'Logger.vb': `Public Module Logger
Public Level As Integer
End Module
`,
'Form1.vb': `Public Class Form1
Public Sub Setup()
Logger.Level = 3
Dim a = Panel1.Size
Me.Panel.Size = New System.Drawing.Size(1, 2)
End Sub
End Class
`,
// CRLF line endings and multibyte text before the read: the resolver reads
// the receiver back out of the source line by column.
'Weird.vb': [
'Public Class Weird',
' Public Sub Go()',
' Dim s = "SessionId éééé 😀" & AppSession.SessionId',
' End Sub',
'End Class',
'',
].join('\r\n'),
};
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 });
});

describe('VB.NET Shared member reads through the class name (#2305)', () => {
it('links a Shared field to every method that reads it', () => {
expect(referencedBy('AppSession::SessionId')).toEqual(
expect.arrayContaining(['Consumer::Run', 'Consumer2::Describe']),
);
});

it('links a Shared property to every method that reads or writes it', () => {
expect(referencedBy('AppSession::CurrentUser')).toEqual(
expect.arrayContaining(['Consumer::Run', 'Consumer2::Describe']),
);
});

it('links the class to the methods that use its members', () => {
expect(referencedBy('AppSession')).toEqual(
expect.arrayContaining(['Consumer::Run', 'Consumer2::Describe']),
);
});

it('keeps the existing call edge to a Shared method', () => {
const id = nodeByQn('AppSession::GetGreeting').id;
const callers = cg.getCallers(id).map((c) => c.node.qualifiedName);
expect(callers).toEqual(['Consumer::Run']);
});

it('links a Module variable read through the module name', () => {
expect(referencedBy('Logger::Level')).toContain('Form1::Setup');
});

it('does not bind a same-named member of a different class', () => {
expect(referencedBy('OtherSession::SessionId')).toEqual([]);
});

it('does not bind a dotted read to a member or class that merely shares its name', () => {
expect(referencedBy('OtherSession::Size')).toEqual([]);
expect(referencedBy('Size')).not.toContain('Form1::Setup');
});

it('resolves on a CRLF file with multibyte text before the read', () => {
expect(referencedBy('AppSession::SessionId')).toContain('Weird::Go');
});
});
15 changes: 13 additions & 2 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ const MEMBER_ACCESS_TYPES: ReadonlySet<string> = new Set([
* already-covered types). Don't re-add `member_expression`/`attribute` here.
*/
const STATIC_MEMBER_LANGS: ReadonlySet<string> = new Set([
'java', 'csharp', 'kotlin', 'swift', 'scala', 'dart', 'php', 'cpp',
'java', 'csharp', 'kotlin', 'swift', 'scala', 'dart', 'php', 'cpp', 'vbnet',
]);

/**
Expand Down Expand Up @@ -5450,7 +5450,18 @@ export class TreeSitterExtractor {
t === 'name' || t === 'scoped_type_identifier'
) {
const text = getNodeText(recv, this.source);
if (/^[A-Z][A-Za-z0-9_]*$/.test(text)) this.pushStaticMemberRef(text, ownerId, recv);
if (/^[A-Z][A-Za-z0-9_]*$/.test(text)) {
this.pushStaticMemberRef(text, ownerId, recv);
// VB.NET (#2305): a Shared field/property read or write is a use of the
// member too, not just of its class. The bare member name is resolved
// against the receiver written on the line (see vbReceiverOf).
if (this.language === 'vbnet' && node.type === 'member_access_expression') {
const member = getChildByField(node, 'member');
if (member?.type === 'identifier') {
this.pushStaticMemberRef(getNodeText(member, this.source), ownerId, member);
}
}
}
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5432,8 +5432,9 @@ export function matchByExactName(
const kotlinBare = kotlinCall && isReceiverLessKotlinCall(ref, context);
const rubyBare = ref.language === 'ruby' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*[?!]?$/.test(ref.referenceName);
const cfmlBare = (ref.language === 'cfml' || ref.language === 'cfscript') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
const vbReceiver = ref.language === 'vbnet' && (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates') && /^\w+$/.test(ref.referenceName)
const vbReceiver = ref.language === 'vbnet' && (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates' || ref.referenceKind === 'references') && /^\w+$/.test(ref.referenceName)
? vbReceiverOf(ref, context) : null;
const vbMemberRead = vbReceiver !== null && ref.referenceKind === 'references';
const objcShape = ref.language === 'objc' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*:*(?:\w+:)*$/.test(ref.referenceName)
? objcCallShape(ref, context) : null;
const csharpBare = ref.language === 'csharp' && (ref.referenceKind === 'calls' || ref.referenceKind === 'references') && /^[A-Za-z_]\w*$/.test(ref.referenceName);
Expand All @@ -5455,6 +5456,7 @@ export function matchByExactName(
!(objcShape === 'self-send' && !isObjcSelfSendTarget(n, ref, context)) &&
!(objcShape === 'super-send' && !isObjcSelfSendTarget(n, ref, context, true)) &&
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&
!(vbMemberRead && !VB_MEMBER_KINDS.has(n.kind)) &&
!(rubyBare && n.kind === 'method' && !isRubyMethodInScope(n, ref, context)) &&
!(cfmlBare && n.kind === 'method' && !isCfmlMethodInScope(n, ref, context)) &&
!(javaBare && n.kind === 'method' && !isJavaMethodInScope(n, ref, context)) &&
Expand Down Expand Up @@ -9246,8 +9248,9 @@ export function matchFuzzy(
const kotlinBare = kotlinCall && isReceiverLessKotlinCall(ref, context);
const rubyBare = ref.language === 'ruby' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*[?!]?$/.test(ref.referenceName);
const cfmlBare = (ref.language === 'cfml' || ref.language === 'cfscript') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
const vbReceiver = ref.language === 'vbnet' && (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates') && /^\w+$/.test(ref.referenceName)
const vbReceiver = ref.language === 'vbnet' && (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates' || ref.referenceKind === 'references') && /^\w+$/.test(ref.referenceName)
? vbReceiverOf(ref, context) : null;
const vbMemberRead = vbReceiver !== null && ref.referenceKind === 'references';
const objcShape = ref.language === 'objc' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*:*(?:\w+:)*$/.test(ref.referenceName)
? objcCallShape(ref, context) : null;
const csharpBare = ref.language === 'csharp' && (ref.referenceKind === 'calls' || ref.referenceKind === 'references') && /^[A-Za-z_]\w*$/.test(ref.referenceName);
Expand Down Expand Up @@ -9283,6 +9286,7 @@ export function matchFuzzy(
!(rubyBare && n.kind === 'method' && !isRubyMethodInScope(n, ref, context)) &&
!(cfmlBare && n.kind === 'method' && !isCfmlMethodInScope(n, ref, context)) &&
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&
!(vbMemberRead && !VB_MEMBER_KINDS.has(n.kind)) &&
!(objcShape === 'c-call' && OBJC_MEMBER_KINDS.has(n.kind)) &&
!(objcShape === 'self-send' && !isObjcSelfSendTarget(n, ref, context)) &&
!(objcShape === 'super-send' && !isObjcSelfSendTarget(n, ref, context, true)) &&
Expand Down